目录
一、参考文章
内容主要出自程杰的《大话数据结构》静态链表一节。
-
“指针”是成熟的编程语言必须具有的概念吗?—— 代码宇宙的回答:https://segmentfault.com/q/1010000003797714/a-1020000003798889
二、静态链表的背景
指针的本意:在一个变量中保存另一个变量的地址,以提供将”地址“变量化的能力。如果没有指针,将无法用一个变量引用另一个变量(只能把变量的值拷贝一份赋给另一个变量)。
C语言提供了完善的指针操作,包括:为指针赋值、内存分配(malloc)、取变量地址、让指针可以参与运算等,这使得C程序员能够任意操作可用内存。
Java、C#等虽不使用指针,但因为启用了对象引用机制,从某种角度也间接实现了指针的某些作用。
但如Basic、Fortran等早期的编程语言是没有指针的,也没有对象引用机制,如此,便无法实现很多基本的数据结构,如链表、树、图等,这些数据结构都必须用指针来保存前驱或后继节点的地址。
那怎么办呢?
使用数组来代替指针,描述单链表。
虽然现在大部分语言都可以靠指针或对象引用机制来实现链表,这种用数据代替指针描述单链表的思想是很独到的,这种用数据描述的链表叫静态链表。思想很简单,实现也很简单,但也确实很巧妙。
三、基本实现
首先,单链表的每个结点都包含要处理的数据和指向后继结点的指针,那么对应数组的元素即为data和cur。data就是要处理的数据,cur相当于next指针,cur也叫游标(Cursor)。
为了方便插入数据,通常把数组建立得大一些,以便有一些空闲空间可以便于插入时不至于溢出。
线性表得静态链表存储结构的代码如下:
#define MAXSIZE 1000 // 假设链表的最大长度是1000
typedef struct {
ElemType data; // 数据
int cur; // 游标
}Component, StaticLinkList[MAXSIZE];
对数组的第一个和最后一个元素作特殊处理,不存数据。
通常把未被使用的数组元素称为备用链表。
数组的第一个元素,即下标为0的元素的cur,就存放备用链表的第一个结点的下标;数组的最后一个元素的cur,存放第一个有数值的元素的下标,相当于单链表中头结点的作用,当整个链表为空时,其为0。如图所示:
初始化数组的代码如下:
// 将一维数组space中各分量链成一备用链表,space[0].cur是头指针
Bool InitList(StaticLinkList space) {
for(int i = 0; i < MAXSIZE - 1; i++) {
space[i].cur = i + 1;
}
space[MAXSIZE - 1].cur = 0; // 目前静态链表为空,最后一个元素的cur为0
return true;
}
假设将数据存入静态链表,比如分别存放着"甲”、“乙”、“丁”、“戊”、“己”、“庚"等数据,如图所示:
此时,”甲“存有下一元素”乙“的游标2,”乙“存有下一元素”丁“的下标3,”庚“是最后一个有值的元素,所以它的cur是0。最后一个元素的cur因为”甲“是第一个有值的元素而存它的下标1。第一个元素因空闲空间的第一个元素下标为7,所以它的cur存7。
四、插入操作
静态链表中要解决的是:如何用静态模拟动态链表结构的存储空间的分配,需要时申请,无用时释放。
在动态链表中,结点的申请和释放分别借用malloc()和free()两个函数来实现。在静态链表中,操作的是数组,不存在像动态链表的结点申请和释放问题,所以需要自己实现这两个函数。
为辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表,每当进行插入时,便可以从备用链表上 取得第一个结点作为待插入的新结点。
节点申请的代码如下:
// 若备用链表非空,则返回分配的结点下标,否则返回0
int Malloc_SLL(StaticLinkList space) {
int i = space[0].cur; // 当前数组第一个元素cur的值,就是要返回的备用链表的第一个元素下标
if (space[0].cur) {
space[0].cur = space[i].cur; // 由于拿出一个分量来使用了,就得把它的下一个分量用来做备用
}
return i;
}
现在如果需要在”乙”和“丁“之间插入一个值为”丙“的元素,按照以前顺序存储结构的做法,应该要把“丁”、“戊”、“己”、“庚"这些元素都往后移一位,但目前不用,因为有了新的手段。
新元素”丙“想插队是吧?可以,让它先排在队伍最后第7个游标的位置,然后,将“乙”原先的cur是游标为3的“丁”,现在改成7,再将“丙”的cur设置为3即可。这样,在绝大多数元素都没有变动的情况下,整个链表的次序发生了改变,如图所示:
插入的代码如下:
// 在L中第i个元素之前插入新的元素e
Bool ListInsert(StaticLinkList L, int i, ElemType e) {
if (i < 1 || i > ListLength(L) + 1) {
return false;
}
int j, k, l;
k = MAX_SIZE - 1; // 注意k首先是最后一个元素的下标
j = Malloc_SLL(L); // 获得空闲分量的下标
if (!j) {
return false;
}
L[j].data = e; // 将数据赋值给此分量的data
for (l = 1; l < i; l++) { // 找到第i个元素之前的位置
k = L[k].cur;
}
L[j].cur = L[k].cur; // 把第i个元素之前的cur赋值给新元素的cur
L[k].cur = j; // 把新元素的下标赋值给第i个元素之前元素的cur
return true;
}
这样,就实现了在数组中,不移动元素,却插入了数据的操作。
五、删除操作
和前面一样,删除元素时,是需要释放结点的函数free()。现在也需自己实现:
// 删除在L中第i个数据元素e
Bool ListDelete(StaticLinkList L, int i) {
if (i < 1 || i > ListLength(L)) {
return false;
}
int j, k;
k = MAX_SIZE - 1;
for (j = 1; j < i; j++) {
k = L[k].cur;
}
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return true;
}
// 将下标为k的空闲结点回收到备用链表
void Free_SLL(StaticLinkList L, int i) {
L[i].cur = space[0].cur; // 把第一个元素cur值赋给要删除的分量cur
L[0].cur = i; // 把要删除的分量下标赋值给第一个元素的cur
}
六、链表长度
静态链表的ListLength实现:
// 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数
int ListLength(StaticLinkList L) {
int j = 0, i = L[MAX_SIZE - 1].cur;
while (i) {
i = L[i].cur;
j++;
}
return j;
}
七、优缺点
-
优点:在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中的插入和删除操作需要移动大量元素的缺点。
-
缺点:
-
没有解决连续存储分配带来的表长难以确定的问题(必须事先知道元素个数来分配内存,单链表则无需如此);
-
失去了顺序存储结构随机存取的特性(原先通过下标可直接存取);
-