上一段落提到了线性表顺序结构的操作及优缺点,其中最大的缺点就是插入和删除时需要移动大量的元素,这样就会消耗时间.那么该怎么解决呢?
顺序存储最大的特点就是相邻,那么能不能不要考虑相邻呢?没错,就这个想法,数据结构中称它为链式存储结构。它的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的.这就意味着这些数据元素可以存储在内存未被占用的任意位置。这也就是说,在存储数据元素的同时,我们需要存储它后继元素的存储地址。
在链式存储结构中,我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域.指针域中存储的信息称做指针或链.由这两部分组成的存储映像就叫做结点(Node).
n个结点链结成一个链表,即为线性表的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫单链表.它正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起.通常,我们把链表中第一个结点的存储位置叫做头指针.为了更为方便的对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点.头结点的数据域可以不存储任何信息。
为了简便表示链表,我们可以这么做:
接下来,我们一起看看头指针和头结点有什么相似点和不同点吧:
若线性表为空表,则头结点的指针域为"空".
单链表中,我们在C语言中可用结构指针来描述:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 typedef struct Node
2 {
3 ElemType data;
4 struct Node *next;
5 }Node;
6 typedef struct Node *LinkList; /* 定义LinkList */
在这个结构定义中,我们也能知道,结点由存放数据元素的数据域和存放后继结点地址的指针域组成.
接着,我们也要像学习顺序存储结构一样,对单链表做一系列的操作,从而更好的理解它.
对单链表来说,它要获取数据元素相比顺序存储结构就麻烦了一些。先来看看实现思路吧,假设要获取的数据元素是第i个:
1、声明一个结点p指向链表的第一个结点,初始化j从1开始
2、当j<i时,遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
3、若找到链表末尾p为空,则说明第i个元素不存在
4、若查找成功,返回结点p的数据。
实现算法如下:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */
2 /* 操作结果:用e返回L中第i个数据元素的值 */
3 Status GetElem(LinkList L,int i,ElemType *e)
4 {
5 int j;
6 LinkList p; /* 声明一指针p */
7 p = L->next; /* 让p指向链表L的第一个结点 */
8 j = 1; /* j为计数器 */
9 while (p && j<i) /* p不为空或者计数器j还没有等于i时,循环继续 */
10 {
11 p = p->next; /* 让p指向下一个结点 */
12 ++j;
13 }
14 if ( !p || j>i )
15 return ERROR; /* 第i个元素不存在 */
16 *e = p->data; /* 取第i个元素的数据 */
17 return OK;
18 }
看到这里,突然发现这个链表太麻烦了,还是用顺序存储结构来得简单,没错,可是one coin has two sides.别着急,我们现在看到的确实很麻烦,不过接下来看完了它其他的操作,就会发现,它有它自己的优势,不然学它干吗呢,对吧~
单链表又是如何对插入删除元素操作的呢?好好思考一下,记住,这是链式存储,不是顺序存储。链式刚刚说了,只需要得到下一个元素的存储位置即可,那么也就是说只需要改变插入点前驱和后继的存储位置即可。
由上图便可以知道,假设e元素的结点是s,那么,只需要这么做就可以了:s->next=p->next;p->next=s;怎么理解呢?其实很简单,将s结点的指针域指向原来p的下一个元素,之后将p的下一个元素指向s。思考:这两行代码可以交换么?答案是不行,原因自己思考思考,这不是难题。
知道了这个原理,我们来总结下插入结点的算法思路:
1、声明一个结点p指向链表的第一个结点,初始化j从1开始
2、当j<i时,遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
3、若找到链表末尾p为空,则说明第i个元素不存在
4、若查找成功,在系统中生成一个空结点s
5、将数据元素e赋值给s->data
6、单链表的插入标准语句:s->next=p->next;p->next=s;
7、返回成功
实现代码如下:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L), */
2 /* 操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1 */
3 Status ListInsert(LinkList *L,int i,ElemType e)
4 {
5 int j;
6 LinkList p,s;
7 p = *L;
8 j = 1;
9 while (p && j < i) /* 寻找第i个结点 */
10 {
11 p = p->next;
12 ++j;
13 }
14 if (!p || j > i)
15 return ERROR; /* 第i个元素不存在 */
16 s = (LinkList)malloc(sizeof(Node)); /* 生成新结点(C语言标准函数) */
17 s->data = e;
18 s->next = p->next; /* 将p的后继结点赋值给s的后继 */
19 p->next = s; /* 将s赋值给p的后继 */
20 return OK;
21 }
插入算法说到这里,那么对应的,我们该看看删除算法了。删除数据元素在单链表中操作特别简单,只需要将要删除的结点的前驱结点的指针绕过本身,直接指向它的后继结点即可。即:p->next=p->next->next。
所以,删除的思路如下:
1、声明一个结点p指向链表的第一个结点,初始化j从1开始
2、当j<i时,遍历链表,让p的指针向后移动,不断指向下一结点,j累加1
3、若找到链表末尾p为空,则说明第i个元素不存在
4、若查找成功,将欲删除的结点p->next赋值给q
5、单链表的删除标准语句:p->next=p->next->next;
6、将q结点中的数据赋值给e作为返回
7、释放q结点
8、返回成功
实现代码如下:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* 初始条件:顺序线性表L已存在,1≤i≤ListLength(L) */
2 /* 操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1 */
3 Status ListDelete(LinkList *L,int i,ElemType *e)
4 {
5 int j;
6 LinkList p,q;
7 p = *L;
8 j = 1;
9 while (p->next && j < i) /* 遍历寻找第i个元素 */
10 {
11 p = p->next;
12 ++j;
13 }
14 if (!(p->next) || j > i)
15 return ERROR; /* 第i个元素不存在 */
16 q = p->next;
17 p->next = q->next; /* 将q的后继赋值给p的后继 */
18 *e = q->data; /* 将q结点中的数据给e */
19 free(q); /* 让系统回收此结点,释放内存 */
20 return OK;
21 }
回顾一下刚刚提到的单链表的插入和删除算法,我们发现,它们其实都是由两部分组成:第一部分是遍历查找第i个元素,第二部分就是真正要做的插入或者删除操作。
分析单链表做插入删除的算法和顺序结构存储做的相同的操作,我们很明显的能看到单链表在这方面的优势。
还记得在讲顺序存储结构的时候,我们在创建时做的工作就是初始化一个数组,那么链式呢?链式结构是动态的,所以创建单链表的过程其实就是一个动态生成链表的过程。同样的,我们先来看看思路:
1、声明一结点p和计数器变量i
2、初始化一空链表L
3、让L的头结点的指针指向NULL,即建立一个带头结点的单链表
4、循环:生成一新结点赋值给p;随机生成一组数字赋值给p的数据域;将p插入到头结点与前一结点之间。
实现代码如下:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* 随机产生n个元素的值,建立带表头结点的单链线性表L(头插法) */
2 void CreateListHead(LinkList *L, int n)
3 {
4 LinkList p;
5 int i;
6 srand(time(0)); /* 初始化随机数种子 */
7 *L = (LinkList)malloc(sizeof(Node));
8 (*L)->next = NULL; /* 先建立一个带头结点的单链表 */
9 for (i=0; i<n; i++)
10 {
11 p = (LinkList)malloc(sizeof(Node)); /* 生成新结点 */
12 p->data = rand()%100+1; /* 随机生成100以内的数字 */
13 p->next = (*L)->next;
14 (*L)->next = p; /* 插入到表头 */
15 }
16 }
在上面这段代码里,我们用的是插队的方法,让新结点总是在第一的位置,也就是头插法。既然可以从头开始,那么是不是也可以从尾巴插入呢?答案是肯定的,这就是尾插法。
实现如下:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* 随机产生n个元素的值,建立带表头结点的单链线性表L(尾插法) */
2 void CreateListTail(LinkList *L, int n)
3 {
4 LinkList p,r;
5 int i;
6 srand(time(0)); /* 初始化随机数种子 */
7 *L = (LinkList)malloc(sizeof(Node)); /* L为整个线性表 */
8 r=*L; /* r为指向尾部的结点 */
9 for (i=0; i<n; i++)
10 {
11 p = (Node *)malloc(sizeof(Node)); /* 生成新结点 */
12 p->data = rand()%100+1; /* 随机生成100以内的数字 */
13 r->next=p; /* 将表尾终端结点的指针指向新结点 */
14 r = p; /* 将当前的新结点定义为表尾终端结点 */
15 }
16 r->next = NULL; /* 表示当前链表结束 */
17 }
所谓有创建就会要删除,那么这个删除要怎么做呢?同样的,我们先看看思路:
1、声明一结点p和q
2、将第一个结点赋值给p
3、循环:将下一结点赋值给q;释放p;将q赋值给p
实现如下:
![ContractedBlock.gif](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![ExpandedBlockStart.gif](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */
2 Status ClearList(LinkList *L)
3 {
4 LinkList p,q;
5 p=(*L)->next; /* p指向第一个结点 */
6 while(p) /* 没到表尾 */
7 {
8 q=p->next;
9 free(p);
10 p=q;
11 }
12 (*L)->next=NULL; /* 头结点指针域为空 */
13 return OK;
14 }
说了这么多,休息一下,我们对这两种存储结构做个总结对比吧:
总之,究竟这两种结构各有优点,谈不上谁好谁坏,根据情况进行使用吧~