此文章仅作为自己学习过程中的记录和总结,同时会有意地去用英文来做笔记,一些术语的英译不太准确,内容如有错漏也请多指教,谢谢!
一、静态链表(Static Linked List):
- 静态链表(Static Linked List):用数组代替指针描述的链表,也称游标实现法。
Each element in a static linked list includes two fields:
1.“data field” for storing data;
2.“cur field”, cursor, for storing the next element’s index, having the same function as the “p->next” above.
静态链表的数组的元素由两个数据域组成:data(数据域)及cur(游标域)。其中,data存放数据元素,cur相当于单链表中的next指针,存放当前元素的后继在数组中的下标。
(为了方便插入数据,我们通常会把数组建立得大一些,可以便于插入时不至于溢出)
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int ElemType; //ElemType的类型根据实际需求而定,此处假设为int。
typedef int Status; /*"Status" is defined as a type of a function, with its returned value representing the result of the function.
(Status是函数的类型,它的返回值势函数结果状态代码,0或1.)*/
#define MAXSIZE 1000
typedef struct
{
ElemType data;
int cur; //cursor(游标), means non-pointing when its value is 0.
} Component, StaticLinkList[MAXSIZE];
另外,我们对数组第一个和最后一个元素作为特殊元素处理,不存储数据。我们通常把未被使用的数组元素称为备用链表(Spare List)。而数组第一个元素,即下标为0的元素的cur就存放备用链表的第一个结点的下标;而数组最后一个元素的cur则存放第一个有数值的元素的下标,其作用相当于单链表中头结点。
二、静态链表部分操作具体实现:
- 初始化静态链表:(将一维数组space中各分量链成一备用链表)
[Initialize a Static linked list.] //将一维数组space中各分量链成一备用链表
Status InitList ( StaticLinkList space )
{
int i;
for( i=0 ; i<MAXSIZE-1 ; i++ ) //"i<MAXSIZE-1"--the last element is special.
{
space[i].cur = i+1;
}
space[MAXSIZE-1] = 0; //Since the list is now empty, the last element's "cur" is 0.
return OK;
}
- 静态链表存储空间的分配:
(须知)
–In dynamic link lists, we can use “malloc()” and “free()” to achieve the application and release of nodes. 在动态链表中,结点的申请和释放可分别借用"malloc()"和"free()"两个函数来实现。
–In static link lists, we operate arrays. So we cannot do the same as above. We need to achieve ourselves. 在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,所以我们需要自己去实现,才能进行插入和删除的操作。
为了辨明数组中哪些分量未被使用,解决的办法是:将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。
- “Malloc_SLL”:T(n)=O(1)
//若备用空间链表非空,则返回分配的结点下标,否则返回0。
int Malloc_SLL ( StaticLinkList space ) //在静态链表中实现malloc()
{
int i = space[0].cur; //The first element's "cur"'s value is the index of the first spare space.
if( space[0].cur )
{
space[0].cur = space[i].cur; /*Since we take one component away, we have to get its next component as the new spare space.
由于要拿出一个分量来使用了,所以我们就得把它的下一个分量拿来作为备用。*/
}
return i;
}
T(n)=O(1)
- “Free_SLL”:T(n)=O(1)
[Achieve the operation of Free() in a SLL] //在静态链表中实现free()
/*Recycle the spare node whose index is "k", and return it to the spare list.*/
void Free_SSL ( StaticLinkList space, int k )
{
space[k].cur = space[0].cur; //把第一个元素的cur值赋给 要删除的元素的cur值,实现“插队”。
space[0].cur = k; //把要删除的元素的下标赋给第一个元素的cur值,使得下次第一个访问的空闲结点就是该删去的结点。
}
T(n)=O(1)
- “ListLength”:T(n)=O(n)
[Achieve the operation of "ListLength" in a SLL]
//INITIAL CONDITIONS: The SLL exists.
//RESULT: Return the number of the elements in list "L".
int ListLength ( StaticLinkList L )
{
int j=0;
int i=L[MAXSIZE-1].cur;
while( i )
{
i = L[i].cur;
j++;
}
return j;
}
- 部分操作具体实现:
- “ListInsert”(插入):T(n)=O(n)
[Achieve the operation of "ListInsert" in a SLL]
/*Insert a new element "e" into list right before No.i element.*/
Status ListInsert ( StaticLinkList L, int i, ElemType e )
{
int j, k, l;
k = MAXSIZE-1;
if( i<1 || i>ListLength(L)+1 )
{
return ERROR;
}
j = Malloc_SSL(L);
if( j )
{
L[j].data = e;
for( l=1; l<=i-1 ; l++ ) //Find the No.i element.
{
k = L[k].cur; //"L[k].cur" means the first element in the spare list.
}
L[j].cur = L[k].cur; //插队
L[k].cur = j; //插队
return OK;
}
return ERROR;
}
T(n)=O(n)
- “ListDelete”(删除):T(n)=O(n)
[Achieve the operation of "ListDelete" in a SLL]
/*Delete the No.i element "e" in list "L".*/
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++ ) //Find the No.i element.
{
k = L[k].cur;
}
j = L[k].cur; //此时k=i-1,即L[k]下一个就是要删除的元素,其下标为j=i-1=L[k].cur。
L[k].cur = L[j].cur; //将要删除的元素的cur赋给前一个元素的cur,即让前一个元素跨过要删除的元素指向下一个元素。
Free_SLL( L, j );
return OK;
}
T(n)=O(n)
三、静态链表优缺点:
- 优点:在插入删除操作时,只需要修改cursor,不需要移动元素,从而改进了顺序表插入删除需要大量移动元素的缺点。
- 缺点:1.没有解决连续存储分配带来的表长难以确定的问题;2.失去了顺序表随机存取的特性。
总体而言:SLL其实是为了给没有指针的高级语言设计的一种实现单链表功能的方法。
四、循环链表(Circular Linked List):
- 循环链表(Circular Linked List):将单链表中终端结点的指针端由NULL改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表成为单循环链表。
(注意:为了使空链表与非空链表处理一致,我们通常设立一个头结点。但这并不是说循环链表一定要有头结点。)
-单链表与循环链表的主要区别:循环的判断条件。
单链表是判断"p->next"是否为空;
循环链表是判断"p->next"是否等于头结点,是的话则循环继续。
此处我们引入一个新概念:尾指针(Rear pointer)。
尾指针是指向终端结点的指针。通过引入尾指针,我们能够将访问表中终端结点的时间复杂度由O(n)降低至O(1)。同时,通过头指针与尾指针,我们能够更方便快速地访问第一个结点以及终端结点。即引入尾指针后,头结点可表示为 “rear->next”, 第一个结点可表示为 “rear->next->next”。
此外,通过引入尾指针,将两个循环链表合并为一个表就非常简便了。以下面两个循环链表为例。
[To combine two circular lists, rear pointers make it much easier]//利用尾指针合并两循环链表
{
p = rearA->next;
rearA->next = rearB->next->next;
q = rearB->next;
rearB->next = p;
free(q);
}
T(n)=O(1)
五、双向链表(Double Linked List):
- 双向链表:在单链表的每个结点中,再设置一个指向其前驱结点的指针域。即双向链表中的每个结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。
[The structure code of Double Link List] //线性表的双向链表存储结构
typedef struct DulNode
{
ElemType data;
struct DulNode *prior;
struct DulNode *next;
} DulNode, *DuLinkList;
双向链表很多操作与单链表相同,如ListLength, GetElem, LocateElem等。主要的区别是在于"插入和删除"操作,因为双向链表每个结点中多了一个指针域。
相同于单链表,双向链表也可以是循环表。以下为非空的双循环链表:
六、双向链表部分操作具体实现:
- "ListInsert"(插入): T(n)=O(1)
[Achieve the operation of "ListInsert" in a Double Link List]
/*RESULT: Insert node"s" into the place between node"p" and node"p->next"*/
//关键在于顺序关系:要先处理s的前驱和后驱,再处理后结点的前驱,才能去处理前结点的后继。不然会导致无法访问后结点。
{
//"s"为新插入的结点,"p"为要插入位置的前一个结点。
s->prior = p;
s->next = p->next;
p->next->prior = s;
p->next = s; //This step must be at the end.
}
T(n)=O(1)
(注意:代码先后顺序很重要。得先搞定新插入结点"s"的前驱和后继,在搞定后结点的前驱,最后再解决前结点的后继。)
- “ListDelete”(删除):T(n)=O(1)
[Achieve the operation of "ListDelete" in a Double Link List]
/*RESULT: Delete node"p" in the list*/
{
p->prior->next = p->next;
p->next->prior = p->prior;
free(p);
}
T(n)=O(1)
七、双向链表总结:
双向链表由于其良好的对称性,使得对某个结点的前后结点的操作更加方便,有效提高了算法的时间性能,但是,空间上由于每个结点多了一个指针域,双向链表是需要占用相对多一些的。
因此可以说是,用空间来换取时间。