数据结构之线性表一主要讲的是线性表的顺序存储结构和链式存储结构的实现和代码。这次我们来讨论下静态链表,循环链表和双向链表。
静态链表
我们让数组每个元素都是由两个数据域组成:data和cur。数据域data用来存储数据元素,cur相当于我们链表中的next指针,存放该数据元素的后继在数组中的下标。我们把这种数组描述的链表叫做静态链表。
基本结构
/*线性表的静态链表存储结构*/
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur; /*游标(Cursor),为0时表示无指向*/
}Component,StaticLinkList[MAXSIZE];
在静态链表中,我们将数组的第一个和最后一个元素作为特殊元素处理,不存放数据。此外,将未被使用的数组元素称为备用链表。而数组的第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标。而数组的最后一个元素的cur则存放第一个有数值元素的下标,相当于链表的头结点作用。
初始化
/*将一维数组space中各分量链成一备用链表*/
/*space[0].cur为头指针*/
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()和free()。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表。每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
/*若备用链表非空,返回分配的结点的下标,否则返回0*/
int Malloc_SLL(StaticLinkList space) {
int i = space[0].cur; /*返回备用链表的第一个结点的下标*/
if(space[0].cur)
space[0].cur = space[i].cur; /*由于使用了一个元素,我们将它的后继链接元素作为备用链表的头*/
return i;
}
静态链表的插入
实现:在L中第i个元素之前插入新元素e
现在我们需要在‘乙’和‘丁’之间,插入一个新元素‘丙’。怎么实现呢?
我们只需要将‘丙’放入备用链表的第一个空位置,也就是下标为7的位置。
另外将‘乙’的游标改为7,‘丙’的游标改为3。
这样实现了不移动元素,完成了插入的动作。
/*在L中第i个元素之前插入新元素e*/
Status ListInsert(StaticLinkList L, int i, ElemType e) {
int j, k ,l; k = MAX_SIZE -1; /*获取数组最后一个位置下标*/
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++)
k = L[k].cur /*获取第i个元素之前位置的下标*/
L[j].cur = L[k].cur;
L[k].cur = j; /*cur值之间的重新链接*/
return OK;
}
return ERROR;
}
静态链表的删除
与内存分配相对应的有内存回收,即将空闲结点回收到备用链表中。
/*将下标为k的空闲结点回收到备用链表*/
void Free_SSL(StaticLinkList space, int k) {
space[k].cur=space[0].cur /*把原来第一个元素游标值赋给要删除的分量
cur*/ space[0].cur = k; /*把要删除的下标值赋给第一个预算的cur*/ }
具体删除操作:
/*删除L中第i个数据元素e*/
Status ListDelete(StatusLinkLiST L,int i) {
int j,k;
if(i<1 || i>ListLength(L))
return ERROR;
k = MAX_SIZE - 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;
}
j=L[999].cur=1;
L[999].cur=L[1].cur=2.
这其实就是告诉计算机‘甲’已经离开了,‘乙’才是第一个元素。
静态链表优缺点:
优点:
* 在插入和删除操作时只需修改游标,不需要移动元素。从而改进了在顺序存储结构中的插入和删除;操作需要移动大量元素的缺点。
缺点:
* 没有解决连续存储分配带来的表长难度以确定的问题;
* 失去了顺序存储结构随机存取的特性。
循环链表
将单链表中终端结点的指针端有空指针改为指向头结点,就是单链表形成一个环,这种头尾相接的单链表称为单循环列表,简称循环列表。循环列表解决了从链表中任意个结点访问链表全部结点的问题。
上图即为非空循环链表,循环链表和单链表的主要差异在于循环的判断上,原来判断p-->next是否为空,现在则是p-->next不等于头结点,则循环未结束。
在单链表中如果我们有了头结点,我们可以用O(1)的时间访问第一个结点,而需要O(n)的时间才能访问到最后一个结点,因为我们需要将整个单链表全部扫描一遍。我们通过改造循环链表可以实现用O(1)的时间来查找头结点和尾结点,我们采用尾指针来表示循环链表,如下图:
上图可以看出,终端结点用尾指针rear来表示,开始结点可以使用rear-->next-->next来表示,这样时间复杂度都是O(1)
假设将两个链表合并成一个的时候,有了尾指针就很方便,比如下面两个循环链表,它们的尾指针分别是rearA和rearB
将它们合并,只需要以下操作,
p=rearA->next ;//保存A表头结点,即①
rearA->next=rearB->next->next //将本是指向B表第一个结点的指针赋值给rearA->next,即②
rearB->next=p;//将原A表的头结点赋值给rearB->next,即③
free(p)//释放p
双向链表
在单链表中我们访问下一个结点的时间复杂度为O(1),可是查找上一个结点的话,最坏的时间复杂度是O(n),因为我们有可能需要从头遍历查找,双向链表是在单链表的每个结点上,在设置一个指向其前驱结点的指针域,所以双向链表结点都有两个指针域,一个指向前驱,一个指向后继。
双向链表带头结点的空链表结构图:
非空的带头结点双向链表结构:
对于一个双向链表,他前驱的后继和他后继的前驱都是他自己,即p->next->prior = p = p->prior->next
双向链表比单向链表多了可以反向查找等数据结构,但是在插入和删除时需要更改的是两个指针变量,
插入操作:假设存储元素e的结点为s,实现s插入结点p和结点p->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->next,如果第四步先执行,则p-next会提前变为s,这样就错误了,所以我们一般插入的顺序:插入结点的前驱和后继---搞定后结点的前驱----前结点的后继
删除操作:删除结点p
p->prior-next = p->next; //把p->next赋值p->prior的后继,图①
p->next->prior = p->prior; //把p->prior赋值给p->next的前驱,图②
线性表结构图