【数据结构之线性表】熬夜暴肝,有亿点详细

简介: 学习线性表,线性表是数据结构中比较简单的一个数据结构了,但是它的重要性不容忽略,废话不多,直接正文。

前言


我们今天来学习线性表,线性表是数据结构中比较简单的一个数据结构了,但是它的重要性不容忽略,废话不多,直接正文。


初识线性表

线性表的定义

线性表是具有相同类型的n(n>=0)个元素的有限序列,其中n为表长,当n=0时,该表为空表


线性表的特点:


1.表中元素个数有限

2.表中元素具有逻辑上的顺序性,在序列中各个元素排序有其先后次序

3.表中元素都是数据元素,每个元素都是单个元素

4.表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间

5.表中元素具有抽象性,即讨论元素间一对一的逻辑关系,而不考虑元素究竟表示的内容

6.线性表是一种逻辑结构,表示元素之间一对一相邻的关系

7.线性表在数据元素有限集中,除第一个元素无直接前驱,最后一个元素无直接后续以外,每个数据元素有且仅有一个直接前驱元素和一个直接后继元素。


线性表的基本操作

还记得之前说过运算的定义依靠数据的逻辑结构实现,运算的实现针对于数据的存储结构吗?不过不记得你可以去看看数据结构入门篇


线性表是一种逻辑结构,因此具体操作我们是无法实现的,不过我们可以定义这些操作。


我们主要从参数,返回值来说一下基本操作吧


&L指传入一个引用,这个引用指向线性表表头


InitList(&L):

初始化表。构造一个空的线性表。


DestroyList(&L):

销毁操作。销毁线性表,并释放线性表L所占用的内存空间。 LocateElem(L,e):

按值查找操作。在表中L查找具有给定关键字值得元素。


GetELem(L,i):

按位查找操作。获取表L中第i个位置的元素的值。


ListInsert(&L,i,e):

插入操作。在表L中的第i个位置上插入指定元素e。


LIstDelete(&L,i,&e):

删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。


PrintList(L):

输出操作。按前后顺序输出线性表L的所有元素值。


Empty(L):

判空操作。若L为空表,则返回TRUE,否则返回FALSE。


Length(L):

求表长。返回线性表L的长度,即L中数据元素的个数。


以上就是线性表的对基本操作的定义,对这些操作的实现我们得依靠存储结构来说了。


线性表根据存储结构的不同又分为顺序表与链表,接下来我们就逐一说一下他们两个吧!


顺序表

顺序表就是线性表的顺序表示,它的主要特点是逻辑结构与存储结构相同都是连续的。


顺序表的定义

一组地址连续存放的存储单元依次存放线性表的元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻


这样一说会感觉这个和数组十分相似啊。的确十分相似,但是却有所不同,详见下文。


顺序表与数组

相同之处:数组与线性表相同都是一组地址连续存放的存储单元依次存放元素


不同之处:数组下标从0开始,而顺序表下标从1开始。数组有最大存储空间是静态的,而顺序表可以是动态的。


定义的实现

顺序表定义的实现有两种分别是静态分配和动态分配。它们的区别是使用静态分配时顺序表的容量是被规定好的,无法增加的。使用动态分配时顺序表的容量可以变大。


实现数组静态分配:


# define MaxSize 100;

typedef struct{

ElemType date[MaxSize];//ElemType是某种元素类型

int length ;//顺序表长度

}sqList;//顺序表名字


我们使用结构体实现顺序表的定义,这里使用了某种类型的数组存放某种数据,数组容量为常量MaxSize。


实现数组动态分配:


# define MaxSize 100;

typedef struct{

ElemType *date;

int length;

}sqList;


动态分配语句:


C: L.data = (Elemtype*)malloc(sizeof(ElemType)*InitSize);

C++:L.data = new ElemType[InitSize]


顺序表的基本操作

顺序表的基本操作有插入,删除,查找


顺序表的插入

这个是给顺序表L在指定位置i处,插入元素e


bool ListInsert (SqList &L,int i,ElemType e){

//除去不合法的i,保证不能空下一个位置

//length+1是因为有可能最后一个位置插入

if(i<1||i>L.length+1){

 return false;

}

if(i>=MaxSize){

 return false;

}

//每个元素向后移动给i-1下标位置空出来

for(int j=L.length;j>=i;j--){

 L.date[j]=L.date[j-1];

}

L.date[i-1]=e;

return true;

}


顺序表的删除

传入要删除的位置和一个元素的引用来保存要删除元素的值


bool ListDelete(SqList &L,int i,ElemType &e){

if(i<1||i>L.length){

 return false;

}

//保存要删除的元素

e=L.date[i-1];

//后面的元素向前逐个移动

for(int j=i;j<L.length;j++){

 L.date[j-1]=L.date[j];

}

L.length--;

return true;

}


顺序表的查找

按值查找元素位置(位置从1开始)


int ListDelete(Sqlist L,ElemType e){

int i;

for(i=0;i<L.length;i++){

 if(L.date[i]==e)

  return i+1;

}

return 0;

}


如果是按位置查找直接L.date[i-1]即可得位置为i处的值是多少。


链表

链表我们最常见的就是单链表,除此之外还有一些特殊链表,我们重点还是说说单链表。


单链表的定义

单链表是线性表的链式存储,通过一组任意的存储单元来存储线性表中的数据元素,通过指针来实现逻辑关系


定义的实现

单链表的每一块都分为数据域和指针域,指针域用来存放逻辑上下一块内存的地址


typedef struct LNode{

ElemType date;

struct LNode *next;

}LNode,*LinkList;


链表又有两种,一种是有头结点的,一种是没有头结点的。我们为了提高效率一般使用带头结点的单链表,后面操作讲解也都是带头结点的链表。


头结点放在链表的最前面,结构与每一块结点相同,不过数据域中为空或者存放链表长度等重要元素,头结点的指针域指向链表的第一个元素。


使用头结点能帮助我们处理头部时方法与其他位置处理方法相同,操作时不需要判断是否为首元素了,大大提高了代码效率。


单链表的基本操作

单链表基本操作有:创建单链表,查找,插入和删除。


创建单链表

创建单链表我们分为头插法创建和尾插法创建,区别就是生成的链表顺序与插入时顺序是否相同。显然头插法创建单链表是顺序相同。

头插法

头插法就是每次插入元素时在链表的头结点后插入,最后生成的链表顺序与插入时顺序相反。


LinkList List_HeadInsert (LinkList &L){

LNode *s;

int x;

L=(LinkList)malloc(sizeof(LNode));

L->next=NULL;

scanf("%d",&x);

while(x!=99999){

 s=(LNode*)malloc(sizeof(LNode));

 s->date=x;

 s->next=L->next;

 L->next=s;

 scanf("%d",&x);

}

return L;

}


尾插法

尾插法就是每次插入元素时在链表的末尾插入元素,最后生成的链表顺序与插入时顺序相同。


LinkList List_HeadInsert (LinkList &L){

LNode *s,*r;

int x;

L=(LinkList)malloc(sizeof(LNode));

L->next=NULL;

*r=L;

scanf("%d",&x);

while(x!=99999){

 s=(LNode*)malloc(sizeof(LNode));

 s->date=x;

 r->next=s;

 r=s;

 scanf("%d",&x);

}

r->next=NULL;

return L;

}


单链表的查找

查找又根据需求分为按序查找和按值查找。


按序查找


如果查找时需要查找某个位置元素的结点,那么就是按序查找。传入的参数为一个链表和需要查找的位置。


LNode *GetElem(LinkList L,int i){

int j=1;

LNode *p=L->next;

if(i==0)

 return L;

if(i<1)

 return NULL;

while(p&&j<i){

 p=p->next;

 j++;

}

return p;

}


按值查找


如果需要在链表中查找某个值e的结点,则:


LNode *LocateElem(LinkList L,ElemType e){

LNode *p=L->next;

while(p!=NULL&&p->date!=e){

 p=p->next;

}

return p;

}


单链表的插入

插入有两种,第一种是插入到指定位置,第二种是默认在链表头部插入

插入到指定位置


要求插入到i位置处时,需要先找到i-1处的结点,然后插入


p=GetElem(L,i-1);

s->next=p->next;

p->next=s;


默认插入头部


在链表头部插入元素时:


s->next=head;

head=s;


单链表的删除

删除i位置的结点:


p=GetElem(L,i-1);

q=p->next;

p->next=q->next;

free(q);


如果需要删除指定结点*q那么只需要:


q=p->next;

p->date=p->next->date;

p->next=q->next;

free(q);


特殊链表

特殊链表有:双链表,循环链表和静态链表。


双链表

双链表顾名思义就是比单链表高级了一点,一个双链表的结点在单链表的基础上又增加了一个指针,这个指针指向它的前一个结点

结点情况如下:


指针域 数据域 指针域

prior date next

定义的实现:


typedef struct DNode{

ElemType date;

struct DNode *prior,*next;

}DNode,*DLinklist;


结点发生改变了。对链表的一些操作也会发生改变

插入操作:


s->next=p->next;

p->next->prior=s;

s->prior=p;

p->next=s;


删除操作:


p->next=q->next;

q->next->prior=p;

free(q);


循环链表

之前的链表都是条状的,循环链表是头尾相连了,就是将链表的尾部与头部连接起来。有因为条状链表有单链表与双链表之分,所以循环链表也有两种分别是:循环单链表与循环双链表


对于循环单链表我们一般设置尾指针这样操作效率会更高。


对于循环双链表我们头结点的prior结点指向最后一个节点,尾结点的next指针指向头结点,以形成循环双链表。


之前的链表我们判断是否为空表都是看最后一个结点的next结点是否为空。显然,循环链表不可以这样判断,我们判断方式如下:


循环单链表判断空链表条件:


if(L->next==L)


循环双链表判断空链表条件:


if(L->next==L&&L->prior==L)


静态链表

静态链表这个名字听起来高大上,其实也是非常简单的,它和数组是非常相似的。先来看它的结构组成:


#define MaxSize 50

typedef struct DNode{

ElemType date;

int next;

}SLinkList[MaxSize];


它其实就是一个数组只不过不连续而已,结点分为两部分。date域存放数据,而next域存放下一个存放数据的数组的下标。


使用数组下标索引实现了逻辑结构。


线性表的常用操作

到此为止我们已经学过了线性表按照存储结构不同的两种分类,分别是顺序表与链表,以及他们的一些基本操作。但是在我们以后在做题时候,不会直接来考插入,删除等操作,它们都是贯穿在很多的大的操作中去的,我愿把他们称为非基本操作。。


下面我们来讲一下对于线性表的一些常考的操作分别是:最值,逆置,归并


最值

最值顾名思义就是求出线性表中的最大以及最小值,我们分别用顺序表与链表实现。


顺序表实现:


int min = L[0];

int max = L[0];

for(int i = 0; i < n; i++){

if(min > L[i])

 min = L[i];

if(max < L[i])

 max = L[i];

}


链表实现:


int min = p -> next ->data;

int max = p -> next ->data;

p = p -> next;

while(p != NULL){

if(min > p ->data)

 min = p ->data;

if(max < p ->data)

 max = p ->data;

p = p ->next;

}


逆置

逆置就是把线性表中元素顺序调反


顺寻表实现


int i = 0;

int j = n-1;

while(i < j){

temp = L[i];

L[i] = L[j];

L[j] = temp;

i++;

j--;

}


链表实现


//r为尾结点

while(p ->next != r){

temp = p -> next;

p -> next = temp -> next;

temp -> next = r -> next;

r -> next = temp;

}


归并

归并就是把两个线性表变为一个,这里其实又有两种了,一种是无序归并,还有一种是有序的归并。无序归并十分简单,这里就不说了,我们主要讲的就是有序归并。


有序归并的栗子:

L1=(1,8,6)

L2=(5,6,9)

归并后为:L3=(1,5,6,6,8,9)


顺序表实现


int i = 0, j = 0;

int k = 0;

for( ; i < L1_Size && j < L2_Size; k++){

if(L1[i] < L2[j])

 L[k] = L1[i++];

else

 L[k] = L2[j++];

}

while(i < L1_Size)

L[k++] = L1[i++];

while(j < L2_Size)

L[k++] = L2[j++];


链表实现归并


while(p->next!=NULL && q->next!=NULL){

if(p->next->data < q->next->data){

 r->next = p->next;

 p->next= p->next->next;

 r = r->next;

}

else{

 r->next = q->next;

 q->next= q->next->next;

 r = r->next;

}

}

if(p->next != NULL) r -> next = p ->next;

if(q->next != NULL) r -> next = q ->next;

free(p);

free(q)


顺序表与链表的比较及选择

到这里我们的顺序表与链表就全部学习完了,那我们在什么时候应该选择顺序表,什么时候又该选择链表呢?我们对顺序表与链表做一个比较来看看吧!!


逻辑结构和物理结构


顺序表:


逻辑相邻物理上也相邻,通过相邻表示逻辑关系


单链表:


逻辑相邻物理上不一定相邻,通过指针表示逻辑关系


基本操作的时间复杂度

插入和删除:


单链表为O(1)(节点指针已知);O(n)(节点指针未知),但操作时只需修改指针


顺序表为O(n)且需要大量移动元素


查找:


按值查找中单链表和顺序表(无序)都为O(n)


按序查找中单链表为O(n),顺序表为O(1)


内存空间

顺序存储:


无论静态分配还是非静态都需要预先分配合适的内存空间 静态分配时预分配空间太大回造成浪费,太小会造成溢出


动态分配时虽不会溢出但是扩充需要大量移动元素,操作效率低


链式存储:


在需要时分配结点空间即可,高效方便,但指针要使用额外空间


如何选择合适的线性表呢?


根据上面的比较做了以下选择总结:


问题情况 顺序表(较稳定) 单链表(较动态)

规模难以估计  *

存储密度大 *

按序号访问 *

插入与删除  *

基于数组 *

基于指针  *

结言

本来打算两天就总结一篇数据结构的,但是我大意了,写起来属实很费劲,很累,甚至一度向放弃。


But我是不会放弃的,下一篇栈与队列,敬请关注!


相关文章
|
5月前
|
存储 C语言
数据结构中的线性表链式存储介绍及其基本操作
链式存储是线性表的一种重要存储方式,它通过节点和指针的结构,实现了灵活的动态存储管理。本文介绍了单向链表的基本操作,并提供了相应的C语言代码示例。理解和掌握链表的操作对学习和应用数据结构具有重要意义。希望这篇博客能帮助你更好地理解线性表的链式存储。
129 2
|
1月前
|
存储 Java
数据结构第二篇【关于java线性表(顺序表)的基本操作】
数据结构第二篇【关于java线性表(顺序表)的基本操作】
30 6
|
19天前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之王道第2.3章节之线性表精题汇总二(5)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
IKU达人之数据结构与算法系列学习×单双链表精题详解、数据结构、C++、排序算法、java 、动态规划 你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
存储
【数据结构】线性表和顺序表
【数据结构】线性表和顺序表
22 1
|
5月前
|
算法
数据结构和算法学习记录——线性表之双向链表(上)-结点类型定义、初始化函数、创建新结点函数、尾插函数、打印函数、尾删函数
数据结构和算法学习记录——线性表之双向链表(上)-结点类型定义、初始化函数、创建新结点函数、尾插函数、打印函数、尾删函数
48 0
|
1月前
01(数据结构考研)线性表相关操作代码
01(数据结构考研)线性表相关操作代码
62 0
|
1月前
|
存储 C语言
数据结构之线性表的初始化及其操作
数据结构之线性表的初始化及其操作
36 0
|
2月前
|
存储 Java
java数据结构,线性表顺序存储(数组)的实现
文章介绍了Java中线性表顺序存储(数组)的实现。线性表是数据结构的一种,它使用数组来实现。文章详细描述了线性表的基本操作,如增加、查找、删除、修改元素,以及其他操作如遍历、清空、求长度等。同时,提供了完整的Java代码实现,包括MyList接口和MyLinearList实现类。通过main函数的测试代码,展示了如何使用这些方法操作线性表。
|
5月前
|
存储 测试技术
【数据结构】操作受限的线性表,栈的具体实现
【数据结构】操作受限的线性表,栈的具体实现
60 5
|
5月前
|
存储 测试技术
【数据结构】操作受限的线性表,队列的具体实现
【数据结构】操作受限的线性表,队列的具体实现
45 4