静态链表
静态链表的定义
用数组代替指针来描述单链表,这样的链表就是静态链表。(游标实现法)
首先让数组的元素都由两个数据域构成,data和cur,即数组的每个下标都对应一个data和一个cur。data是数据域,用来存放数据元素,cur相当于指针域,用来存放后继元素在数组中的下标,所以又称cur为“游标”。
综上所述,其代码描述如下:
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur;
}Component,StaticLinkList[MAXSIZE];
之后要对静态链表进行初始化:第一个和最后一个元素作为特殊元素是不存数据的。我们将未被使用的数组元素称作备用链表,数组的第一个元素的cur就用于存放备用链表的第一个结点的下标,数组的最后一个元素的cur存放第一个有数值的元素的下标。
初始化代码如下:
Status InitList(StaticLinkList space)
{
int i;
for(i=0;i<MAXSIZE-1;i++)
space[i].cur=i+1;
space[MAXSIZE-1].cur=0;
return true;
}
使用时也要注意以下两点:
- 当静态链表为空时,第一个元素cur中存放的下标为1,而最后一个元素cur中存放的下标为0。
- 如果该元素没有下一个元素,则其cur中存放的下标为0。
静态链表的插入操作
在动态链表中,结点的申请和释放分别使用malloc()和free()实现,而在静态链表中我们操作的是数组,无法使用上述的两个函数进行操作,那么插入和删除的函数就要自行定义实现。
在定义中我们有提及备用链表这一概念,在静态链表中备用链表就代表着未被使用的数组空间,这些空间利用下标和cur形成了类似链表的结构,当我们要将新的元素插入到静态链表中时,就需要用到备用链表中的空间。
而我们对插入的详细操作就是在插入时从备用链表上取得第一个结点作为新结点,并且将这个结点的cur中存储的下标给space[0].cur(即第一个结点的cur),以保持备用链表的连续性,并根据插入的位置将新结点的cur改为前驱结点的cur,前驱结点的cur改为新结点的下标。
详细实现代码如下:
/*找出从备用链表中取出新结点*/
int Malloc_SLL(StaticLinkLIst space)
{
int i=space[0].cur;
if(space[0].cur) /*当备用链表不为空的时候*/
space[0].cur=sapce[i].cur;
return i;/*将新结点的下标返回*/
}
/*得出静态链表中数据元素的个数*/
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)/*当i为0时,到数据元素的最后一个元素,跳出循环*/
{
i=L[i].cur;
j++;
}
reutrn j;
}
/*在L中-第i个元素-之前插入新的数据元素e*/
Status ListInsert(StaticLinkList L,int i,ElemType)
{
int j,k,l;
k=MAX_SIZE-1;
if(i<1 || i>ListLength(L)+1)
return false;
j=Malloc_SLL(L);/*新结点*/
if(j)
{
L[j].data=e;
for(l=1;l<i-1;l++)
k=L[k].cur;/*找出第i-1个结点,即第i个结点的前驱结点*/
L[j].cur=L[k].cur;
L[k].cur=j;
return true;
}
return false;
}
静态链表的删除操作
删除操作也需要对备用链表进行操作,在删除结点时,要将删除的结点数组空间再放入备用链表中,已存数据的数组空间也要进行调整。
详细操作思路:
- 将被删除结点的cur赋值给它的前驱结点的cur
- 将要备用链表第一个结点的cur赋值给被删除结点的cur
- 将被删除结点的下标赋值给备用链表第一个结点的cur
上述第1步就是将已存数据的空间进行调整,将被删除结点移出存储数据元素的链表,第2-3步其实是将备用链表中的空间进行调整,将被删除结点插入到备用链表的第一个结点之后(头结点之后)。
其详细代码如下:
/*将被删除结点插入到备用链表中*/
void Free_SSL(StaticLinkLIst space,int k)
{
space[k].cur=space[0].cur;
space[0].cur=k;
}
/*删除静态链表中第i个数据元素e*/
Status ListDelete(StaticLinkList L,int i)
{
int j,k;
if(i<1 || i>ListLength(L))
return false;
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_SLL(L,j);
return true;
}
静态链表优缺点分析
优点:插入和删除时仅需改动游标,相较于顺序存储结构拥有着不需要移动数据元素的优点。
缺点:表长难以确定,需要编写函数得到。失去了顺序结构随机存取的优点。
静态链表其实是为了给没有指针的高级语言设计的一种实现单链表的方法。
循环链表
将单链表中终端结点的指针域指向改为指向头结点,使得整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表。
循环链表不一定要有头结点,但是有头结点的话,其进行循环的判断条件就是p->next不等于头结点。
但是在使用时,如果我们要得到终端结点的话,需要进行循环,其时间复杂度为O(n),为了解决这个问题,我们可以设置一个尾指针rear来指向终端结点,这样得出终端结点的时间复杂度为O(1),得到开始结点仅需rear->next->next(有头结点的情况下),其时间复杂度也为O(1)。
循环链表的插入和删除与链式存储结构的插入存储操作大致相同,不再赘述。
双向链表
双向链表的定义
双向链表是在单链表的每个结点中,增加一个指向其前驱结点的指针域。
其结构如下:
typedef struct DulNode
{
ElemType data;
struct DulNode *prior;
struct DulNode *next;
}DulNode,*DuLinkList;
双向链表的操作
插入:与单链表相差不大,只是需要同时对前驱结点进行操作。
双向链表插入核心的代码:
/*要将结点s插入到结点p和p->next之间*/
s->prior=p;
s->next=p->next;
p->next->prior=s;
p->next=s;
顺序为先处理s的前驱和后继,之后再处理后继结点的前驱,最后处理前驱结点的后继。将前驱结点的操作放在最后,可以使p->next在复制前始终指向后继结点,不会发生错误。
相较于插入,删除操作会简单一些,因为不需要考虑被删除结点的前驱和后继赋值。
双向链表删除的核心代码:
/*要删除结点p*/
p->prior->next=p->next;
p->next->prior=p->prior;
free(p);
对于双向链表的操作,最需要注意的地方还是操作的顺序,时刻注意每个指针指向什么,这样就可以减少出错的可能性。