静态链表:用数组代替指针来描述单链表,也可以叫做游标实现法。
数据全部存储在数组中(和顺序表一样),但存储位置是随机的,数据之间"一对一"的逻辑关系通过一个整形变量(称为"游标",和指针功能类似)来访问。
#define MAXSIZE 1000
typedef struct{
ElemType data;
int cur; // 游标(cursor),为0时表示无指向
}Component,StaticLinkList[MAXSIZE];
静态链表的特殊处理:对第一个和最后一个元素作为特殊元素处理,不存数据。通常把未被使用的数组元素称为备用链表。
静态链表的初始化操作:
// 将一维数组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()
和free()
两个函数来实现;而在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,需要我们自己实现这两个函数,才可以实现插入和删除操作。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第—个结点作为待插入的新结点。
int Malloc_SSL(StaticLinkList space){
int i = space[0].cur; // 当前数组第一个元素的cur存的值,即第一个备用空闲的下标
if(space[0].cur){
space[0].cur = space[i].cur; //拿出备用链表的一个分量来使用,将其作为下个分量
}
return i;
}
插入示意图如图所示:
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个元素之前元素的cur
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;
}
意思就是“甲”现在要走,这个位置就空出来了,也就是,未来如果有新人来,最优先考虑这里,所以原来的第一个空位分量,,即下标是8的分量,它降级了,把8给“甲“,所在下标为1的分量的cur,也就是space[1].cur≡space[0].cur=8,而space[0].cur=k=l,其实就是让这个删除的位置成为第—个优先空位,把它存入第一个元素的cur中,如下图所示。
ListLength():
/* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
i=L[i].cur;
j++;
}
return j;
}
优点:
缺点:
循环链表:将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
循环链表解决了一个很麻烦的问题:如何从当中一个结点出发,访问链表的全部结点。
循环链表带有头结点的空链表如下图所示:
对于非空的循环链表如下图所示:
要将两个循环链表合并成一个表时,有了尾指针就非常简单了。比如下面的这两个循环链表,它们的尾指针分别为rearA
和rearB
,如下图所示:
要将它们合并,只需要如下的操作即可。
p = rearA->next; // 保存A表的头结点,即步骤1
rearA->next = rearB->next->next; // 1. 将本是指向B表的第一个结点(不是头结点) 2. 复制给rearA->next,即步骤2
q = rearB->next;
rearB->next = p; // 将原A表的头结点复制给rearB->next,即步骤3
free(q); // 释放q
在单链表中,有了next指针,使得我们要查找下一结点的时间复杂度为 O ( 1 ) O(1) O(1),但是如果我们要查找上一结点的话,最坏的时间复杂度就是 O ( n ) O(n) O(n)。
双向链表(double linked list):在单链表的每个结点中,再设置一个指向其前驱结点的指针域。所以在双向链表的结点中都有两个指针域,一个指向直接后继,另一个指向直接前驱。
双向链表定义:
typedef struct DulNode{
ElemType data;
struct DulNode *prier; // 直接前驱指针
struct DulNode *next; // 直接后继指针
}DulNode,*DuLinkList;
双向链表的循环带头结点如图所示:
非空循环带头结点的双向链表如图所示:
双向链表是单链表中扩展出来的结构,所以它的很多操作是和单链表相同的,比如求长度的ListLength,查找元素的GetElem,获得元素位置的LocateElem等,这些操作都只要涉及一个方向的指针即可,另—指针多了也不能提供什么帮助。
插入操作很简单,但是顺序很重要。现在假设存储元素e的结点为s,要实现将结点s插入到结点p和p->next之间的步骤如图所示:
s -> prior = p; // 将p赋值给s的前驱,步骤1
s -> next = p -> next; // 把p->next赋值给s的后继,步骤2
p -> next -> prior = s; // 把s赋值给p->next的前驱,步骤3
p -> next = s; // 把s赋值给p的后继,步骤4
删除操作也很简单,若要删除结点p,只需要下面两个步骤,如图所示:
p->prior->next = p->next; // 把p->next赋值给p->prior的后继,步骤1
p->next->prior = p->prior; // 把p->prior赋值给p->next,步骤2
free(p); // 释放结点