第二章:线性表(链式表示)
前面讲到线性表的顺序表示也就是顺序表,顺序表虽然可以随机存储,但是在初始化的时候需要申请一大块连续的存储空间,且在执行插入和删除操作时,也需要大量的移动元素,时间复杂度比较高,下面讲线性表的另一种存储结构:链式存储
1.单链表的定义
线性表的链式存储又称:单链表 ,通过一组任意的存储单元来存储线性表中的数据元素。
数据元素存储在任意位置,不一定连续,通过指针实现线性逻辑关系。
我们把单链表中这样 数据加地址的组合 叫做单链表的一个结点,一个结点存储数据元素的数据域和下一个结点(数据元素)的地址的指针域组成。
![ce4bd0f226cfcb7ec0a8d7f529ca1393.png](https://img-blog.csdnimg.cn/img_convert/ce4bd0f226cfcb7ec0a8d7f529ca1393.png)
单链表有两种创建方式
- 无头结点的单链表
- 有头节点的单链表
有头节点的单链表,它的头节点的数据域一般不存储数据,它的指针域存储第一个结点的地址。优点:
- 链表的第一个位置和其他位置的操作统一(比如插入操作:无头节点的链表,在表中插入结点的时候两边都有结点,而在表头插入结点的时候左边是没有结点的,而有头节点就都是一样的。)
- 空表和非空表的操作统一
![9acccc7955b930f3a9d1cdf8e9aec877.png](https://img-blog.csdnimg.cn/img_convert/9acccc7955b930f3a9d1cdf8e9aec877.png)
2.单链表的基本操作
2.1头插法建立
![5348573835e00061ed8a3608be493a76.png](https://img-blog.csdnimg.cn/img_convert/5348573835e00061ed8a3608be493a76.png)
//头插法LinkList List_HeadInsert (LinkList &L){ LNode *s; int x; L=(LinkList)malloc(sizeof(LNode)); L->next=NULL; scanf("%d",&x); while(x!=9999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; s->next=L->next; L->next=s; scanf("%d",&x); } return L;}
![13be2aefa0912118a5f3c728aca4cce2.png](https://img-blog.csdnimg.cn/img_convert/13be2aefa0912118a5f3c728aca4cce2.png)
时间复杂度:O(n)
2.2尾插法建立
![daae510479ac863fd8931c7ef77dd847.png](https://img-blog.csdnimg.cn/img_convert/daae510479ac863fd8931c7ef77dd847.png)
LinkList List_TailInsert (LinkList &L){ int x; L=(LinkList)malloc(sizeof(LNode)); LNode *s,*r=L; //注意这里重新定义一个指针r,作为尾指针,同时初始化为了头节点 scanf("%d",&x); while(x!=999){ s=(LNode*)malloc(sizeof(LNode)); s->data=x; r->next=s; r=s; //修改尾指针,指向新插入的结点 scanf("%d",&x); } r->next=NULL; return L;}
时间复杂度:O(n)
2.3按序号查找&按值查找
按照和按序号查找都要遍历单链表。
按序号查找
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&&jnext; j++; } return p;}
时间复杂度:O(n)
按序值查找
LNode *LocateElem(LinkList L,ElemType e){ //初始化一个指针指向头节点的下一个结点 LNode *p=L->next; //判断结点不为空,且数据不为e while(p!=NULL&&p->data!=e){ //如果结点不为空,且值不为e,则指针继续向下移动 p=p->next; } return p;}
时间复杂度:O(n)
2.4插入结点
插入有两种方式,前插法和后插入法,比如插入位置为 i ,前插法就是在 i 的位置之前插入, 后插法就是在 i 的位置之后插入,所以前插法要找 i-1 位置,而后插法要找 i 的位置。所以如果 i 的位置是已知的,这样两种方法就会产生区别,前插法仍然需要遍历链表O(n),而后插法直接使用这个位置即可O(1)。后插法是可以实现前插法的,插入之后交换两个结点的位置即可。下面演示前插法:
插入结点首先要知道插入的位置,假如插入位置为 i ,则需要知道 i-1 结点的位置。接着修改新结点的指针指向 i-1结点的下一个位置,然后修改i-1结点的指针指向新插入结点。
![e8ba4096e6e56d57875777337aa2a861.png](https://img-blog.csdnimg.cn/img_convert/e8ba4096e6e56d57875777337aa2a861.png)
注意 下面的代码,不能交换位置。
s->next=p->next;p->next=s;
why??? 这个顺序是不能交换的,如果交换会出现i结点地址丢失的问题。p->next=s;这时已经将p结点中存储的i结点的地址给覆盖了,成了新结点的地址,接着再s->next=p->next;这相当于讲s结点的指针指向了他自己。这样就把后面的链表给丢弃了。
2.5删除结点
结点的位置未知
假如要删除链表中第 i 号结点的位置,修改第 i-1号结点的指针,让其指向第 i+1号结点的位置。这是要注意要使用一个指针指向第i个结点,因为修改之后我们会失去第 i 个结点的位置,这样后面就无法释放i结点的空间。
![d950b954ecd4eee873bfdae46d3dcedf.png](https://img-blog.csdnimg.cn/img_convert/d950b954ecd4eee873bfdae46d3dcedf.png)
结点的位置已知*p
这时可以先交换p结点和后一个结点的数据,然后删除后一个结点即可,注意要有一个指针指向后一个结点,方便之后释放空间。
![418bcbb067bdc3e8ecc9e14979b672c5.png](https://img-blog.csdnimg.cn/img_convert/418bcbb067bdc3e8ecc9e14979b672c5.png)
在这里插入图片描述
2.6求表长
有头节点和无头节点的链表判断是不一样的。
![b840b7a6466dc5d296212200c345cce9.png](https://img-blog.csdnimg.cn/img_convert/b840b7a6466dc5d296212200c345cce9.png)
3.特殊链表
3.1双链表
在使用单链表的时候,我们知道当前结点i的指针,在执行插入,删除等需要知道它的前驱结点的操作,我们需要通过按序号查找的方式查找到他的前驱结点。这样时间复杂度是O(n)。
![0a689c84c610cbc6c14ab4eea5c64de1.png](https://img-blog.csdnimg.cn/img_convert/0a689c84c610cbc6c14ab4eea5c64de1.png)
在这里插入图片描述
如果节点中有一个直接指向它的前驱结点的指针,那么我们就可以直接找到它的前驱节点了。所以这样就出现了双链表。
![6a1ec0f617bde9df0ad6483a7b9e31bd.png](https://img-blog.csdnimg.cn/img_convert/6a1ec0f617bde9df0ad6483a7b9e31bd.png)
![e57f350ef1a567696462c6f3fc43404b.png](https://img-blog.csdnimg.cn/img_convert/e57f350ef1a567696462c6f3fc43404b.png)
3.1双链表插入操作
![5e51720a34bc264e88c8008431fd7b39.png](https://img-blog.csdnimg.cn/img_convert/5e51720a34bc264e88c8008431fd7b39.png)
前插法和后插法的时间复杂度都是O(1)。这个插入顺序是可以调的,但是第一个和第二步,必须在第四步之前,因为第二步我们需要i+1结点的位置。
在单链表中,在表头、表中和表尾的插入步骤相同。但是注意在双链表中,在表头和表中跟在表尾插入是不一样的步骤。在双链表的表尾进行插入的时候要注意表尾后面没有下一个结点,所以不能修改下一个结点指向前驱的指针,否则会出现错误。
3.1双链表删除操作
首先找到要删除的结点(q)的前驱结点的指针,设为p,接着直接修改指针,释放空间即可。时间复杂度为O(1)。同样在表尾进行删除的时候也是不一样的。
![3967904e7996ac69ce8e8e420a53f0dd.png](https://img-blog.csdnimg.cn/img_convert/3967904e7996ac69ce8e8e420a53f0dd.png)
3.2循环链表
3.2.1循环单链表
假设我们使用单链表,我们只知道尾指针,但是需要知道头指针,这时候是无法知道的。这是如果将单链表的最后一个结点的指针指向头节,这样就可以找到的头指针,这样的链表形成一个环,叫做循环单链表。这样就只设置一个尾指针就行了,而且效率更高:因为如果只有头指针,我们想找到尾指针,需要遍历单链表,但是如果有一个尾指针,我们找头指针直接就可以找到。在循环单链表插入和删除操作,在每一个位置都是一样。
![f10f1dffa09bf9dab2b1ee90e26fcc0c.png](https://img-blog.csdnimg.cn/img_convert/f10f1dffa09bf9dab2b1ee90e26fcc0c.png)
在这里插入图片描述
3.2.1循环双链表
我们需把链表最后一个结点的指针修改为头节点,且需要修改头节点的前驱指针指向最后一个结点。这时每一个位置的插入和删除操作都是一样。
![dd661b005688ca260ed5634dc6aa8c14.png](https://img-blog.csdnimg.cn/img_convert/dd661b005688ca260ed5634dc6aa8c14.png)
3.2.3循环链表判空
我们发现在循环链表中,我们利用了每一个结点的指针,也就是说在循环链表中,没有空指针了,这时该怎么判空呢??请看下图:
![fd16ffe58dc1f7c7306bc9724997ff4b.png](https://img-blog.csdnimg.cn/img_convert/fd16ffe58dc1f7c7306bc9724997ff4b.png)
3.3静态链表
静态链表:就是使用数组来实现的链式存储结构的链表。
单链表:
![45632b8b7b292cf0664f4d19f780177c.png](https://img-blog.csdnimg.cn/img_convert/45632b8b7b292cf0664f4d19f780177c.png)
静态链表:
![f799da40c4cc30b4a7bdf6ff015dbdde.png](https://img-blog.csdnimg.cn/img_convert/f799da40c4cc30b4a7bdf6ff015dbdde.png)
静态链表中每个结点既有自己的数据部分,还需要存储下一个结点的位置,所以静态链表的存储实现使用的是结构体数组,包含两部分:数据域 和 游标(存放的是下一个结点在数组中的位置下标)。
#define MaxSize 50typedef struct DNode{ ElemType data; int next;}SLinkList[MaxSize];
关于数据结构的知识,持续更新中,欢迎关注公众号理木客