上一章我们学习了线性表的链式存储结构以及链式存储结构中的一种——单链表,本文章将讨论静态链表,循环链表以及双向链表
写在前面
首先,学习历程重重困难,文章一定会难以避免的冗长,所以在前面先附上我制作的思维导图,这个是线性表总的思维导图hh(包括链表),大家可以先点进去看,思维导图是用的知犀思维导图,不过我觉得好像不用下载那个点开链接输入密码后也可以看,我觉得思维导图的表现力应该比文字更好hh,所以大家可以先看这个思维导图,有个大致的思路hh
思维导图在这里!密码:1836
正文
静态链表
对于一些早期语言,没有指针的概念,那我们应该如何实现单链表呢?我们可以用数组来实现链式结构,用数组来描述的链表,就叫做静态链表,而这种实现方法,我们叫它游标实现法
为此,我们可以实现一个静态存储结构~
#define MAXSIZE 1000 /* 存储空间初始分配量 */
/* 线性表的静态链表存储结构 */
typedef struct
{
ElemType data;
int cur; /* 游标(Cursor) ,为0时表示无指向 */
} Component,StaticLinkList[MAXSIZE];
另外,我们对数组第一个和最后一个元素作为特殊元素处理,不存数据,我们通常将未被使用的元素称为备用链表
而链表的第一个元素,及下标为零的元素cur就存放备用链表的第一个结点的下标,而数组的最后一个元素的cur则存放第一个有数值的元素的下标,相当于单链表中的头结点,当整个链表为空时,其为零
故而我们可以这样初始化:
/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space)
{
int i;
for (i=0; i<MAXSIZE-1; i++)
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
return OK;
}
现在我们来看看元素的插入
静态链表中要解决的问题是:如何用静态模拟动态链表结构的存储空间的分配。
由于我们操作的是数组,故而无法像之前的结点申请那样借用malloc()
函数,故而我们可以将所有未被使用的以及被删除的分量用游标连成一个备用的链表,每当插入时,便可以从备用链表上去的第一个结点作为待插入的新结点
我们返回未使用结点(第一个空闲下标)的代码如下:
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space)
{
int i = space[0].cur; /* 当前数组第一个元素的cur存的值 */
/* 就是要返回的第一个备用空闲的下标 */
if (space[0]. cur)
space[0]. cur = space[i].cur; /* 由于要拿出一个分量来使用了, */
/* 所以我们就得把它的下一个 */
/* 分量用来做备用 */
return i;
}
而对于要插队的元素,我们只需要将它排在最后,然后修改一下下标cur即可
及我们将“丙”排到队尾,然后让“乙”的下标改成7,最后让“丙”的下标改为“乙”的后一位也就是3即可
代码如下:
/* 在L中第i个元素之前插入新的数据元素e */
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE - 1; /* 注意k首先是最后一个元素的下标 */
if (i < 1 || i > ListLength(L) + 1)
return ERROR;
j = Malloc_SSL(L); /* 获得空闲分量的下标 是前面所实现了的函数*/
if (j)
{
L[j].data = e; /* 将数据赋值给此分量的data */
for(l = 1; l <= i - 1; l++) /* 找到第i个元素之前的位置 */
k = L[k].cur;
L[j].cur = L[k].cur; /* 把第i个元素之前的cur赋值给新元素的cur */
L[k].cur = j; /* 把新元素的下标赋值给第i个元素之前元素的ur */
return OK;
}
return ERROR;
}
对于删除元素,和之前一样,原来是需要释放结点的free()
现在我们也只能自己实现它:
首先,我们也是要找到要删除的分量的下标并将它整到备用链表中:
/* 将下标为k的空闲结点回收到备用链表 */
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; /* 把第一个元素的cur值赋给要删除的分量cur */
space[0].cur = k; /* 把要删除的分量下标赋值给第一个元素的cur */
}
其次,我们就要修改某几个元素的下标
/* 删除在L中第i个数据元素 */
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i < 1 || i > ListLength(L))
return ERROR;
k = MAXSIZE - 1;
for (j = 1; j <= i - 1; j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
当然,静态链表也有其他操作的相关实现,比如我们的长度:
/* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
i=L[i].cur;
j++;
}
return j;
}
总的来说,静态链表其实是为了给那些没有指针的高级语言实现的一种单链表能力的方法,对于上述代码,我们可以在以下main函数中实现它们:
#include "string.h"
#include "ctype.h"
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#include "time.h"
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef char ElemType; /* ElemType类型根据实际情况而定,这里假设为char */
int main()
{
StaticLinkList L;
Status i;
i=InitList(L);
printf("初始化L后:L.length=%d\n",ListLength(L));
i=ListInsert(L,1,'F');
i=ListInsert(L,1,'E');
i=ListInsert(L,1,'D');
i=ListInsert(L,1,'B');
i=ListInsert(L,1,'A');
printf("\n在L的表头依次插入FEDBA后:\nL.data=");
ListTraverse(L);
i=ListInsert(L,3,'C');
printf("\n在L的“B”与“D”之间插入“C”后:\nL.data=");
ListTraverse(L);
i=ListDelete(L,1);
printf("\n在L的删除“A”后:\nL.data=");
ListTraverse(L);
printf("\n");
return 0;
}
循环链表
将单链表中的终端结点的指针端由空指针改为指向头结点,使整个单链表形成一个环,这就是循环链表
循环链表解决了一个很棘手的问题:如何从某个结点开始,遍历链表中的每个元素。
为了使空链表与非空链表处理一致,我们通常设一个头结点,当然,这并不是说循环链表一定要有头结点
带有头结点的循环空链表如下图所示:
在单链表中,是判断p->next
是否为空,现在则是p->next
不等于头结点,则循环未结束
我们可以改造一下循环链表,不用头指针,而用一个尾指针(红色所示)
这样我们探索第一个和最后一个结点的复杂度就都是o(1),尾指针用rear
表示,则终端结点就是rear
而开始结点就是rear->next->next
若是将两个循环链表合成一个表,有了尾指针就好办了,比如我们现在有两个循环链表,尾指针分别是rearA和rearB则可以这样:
p=rearA->next; /* 保存A表的头结点,即① */
rearA->next=rearB->next->next; /* 将本是指向B表的第一个结点(不是头结点)*/
/* 赋值给rearA->next,即② */
q=rearB->next;
rearB->next=p; /* 将原A表的头结点赋值给rearB->next,即③ */
free(q); /* 释放q */
双向链表
为了克服单向性的缺点,双向链表就是在单链表的每个结点中,再设置一个指向其前驱结点的指针域
故而我们的双向链表的循环带头结点的空链表如下图所示:
非空的循环带头结点的双向链表:
我们可以由此写出双向链表的存储结构:
/*线性表的双向链表存储结构*/
typedef struct DulNode
{
ElemType data;
struct DuLNode *prior; /*直接前驱指针*/
struct DuLNode *next; /*直接后继指针*/
} DulNode, *DuLinkList;
对于每一个结点p,它的后继的前驱和他前驱的后继都是他自己,也就是说p->next->prior = p = p->prior->next
插入操作时,我们需要更改两个指针变量,插入操作时并不复杂,不过顺序很重要,不可以写反了:
s - >prior = p; /*把p赋值给s的前驱,如图中①*/
s -> next = p -> next; /*把p->next赋值给s的后继,如图中②*/
p -> next -> prior = s; /*把s赋值给p->next的前驱,如图中③*/
p -> next = s; /*把s赋值给p的后继,如图中④*/
如果要是删除操作,则只需两步:
p->prior->next=p->next; /*把p->next赋值给p->prior的后继,如图中①*/
p->next->prior=p->prior; /*把p->prior赋值给p->next的前驱,如图中②*/
free(p); /*释放结点*/
说白了,双向链表对比于单链表结构确实复杂,但是由于良好的对称性给前后结点的操作都带来了方便,简单说,就是用空间换时间
总结
本节我们介绍了静态链表,循环链表,双向链表,其中,静态链表我们一般不会用到,而循环链表和双向链表则是一种升级。
我们到现在已经介绍完了线性表的两种结构,它们是数据结构的基础,对后面的学习有着至关重要的作用