前言
(本文参考严蔚敏《数据结构》C语言版,针对静态单链表提供了更为详细的分析,并加入了一些自己的风格与更详细注释,供初学者参考讨论)
在了解静态单链表之前,我们先简要回顾一下顺序表和动态单链表:
1)顺序表:
数组实现,用一组地址连续的存储单元依次存储线性表的数据元素。利用 realloc 和 LISTINCREMENT(块)实现变长数组(resizable array)
特点:逻辑上相邻的两个元素在物理位置上也相邻。
优点:允许随机访问(通过下标),“求表长”和“取第 i 个元素”时间复杂度O(1)。
缺点:执行删除或插入时要移动大量元素。
2)动态单链表:
链式实现,节点(结点)分为 数据域 和 (单个)指针域(指向直接后继元素),通过指针域的指针(链)将 n 个结点链成一个链表。
因为链表的长度可用 malloc 和 free 自由控制,故称其为“动态”。
特点:用一组任意的(连续或不连续)存储单元存储线性表的数据元素。
优点:对存储空间的利用率高,插入和删除方便。
缺点:不支持随机访问。
本人创建链表的习惯:(下例为双向链表)
typedef struct _polynomical {
int coe;//系数 coefficient
int expo;//指数 exponent
struct _polynomical *next;//!!!切记 struct
struct _polynomical *prev;//!!!切记 struct
} polynomical;
typedef struct _poly {
unsigned size;//项数
polynomical *head;
polynomical *tail;
} Poly;
3)静态单链表
静态单链表就像是顺序表和动态单链表的结合版:利用了顺序表的一维数组描述方法,又有动态单链表的类似结点结构和访问特性(指针与游标)。
“静态”的含义:一般静态单链表只需 define 一个 MAXSIZE,之后链表大小不再改变。不需要像顺序表一样 resizable,因为它能像动态单链表一样重复利用空间(注意静态单链表并没有“释放”空间,而是利用了一个备用链表)
关键1:游标cursor
关键2:备用链表
意义:静态单链表的描述方法便于在不设“指针”类型的高级程序设计语言中使用链表结构。
静态单链表创建
#define MAXSIZE 100//链表的最大长度
typedef struct {
int data;
int cur;//!!!游标:(cursor)其值为下个元素的数组下标(等效于指针)
} component, SLinkedList[MAXSIZE];
静态单链表的第一个关键点就是游标的使用:
在某些高级程序设计语言中并没有“指针”,此时 int 型的游标就可以发挥指针的作用,即在每个结点内存储了下一个结点在结构数组中的位置。
main()中创建:
SLinkedList ls;
结构分析
参考严蔚敏《数据结构》
我们可以与动态单链表做类比:
分析:
若令 i = S[0].cur,则 S[i].data 为第一个数据元素
i = S[i].cur 类似于 p = p->next
例如元素的查找:
//在静态链表查找第一个 e 元素
//找到则返回位序,否则返回 0
int LocateElem_SL(SLinkedList S, int e){
int i = S[0].cur;// S[0].cur 的作用相当于头指针
while (i && S[i].data != e) {
i = S[i].cur;//相当于 p = p->next;
}
return i;
}
静态单链表的删除和插入(前奏)
关键问题:提高空间利用率,又为了避免大量的元素移动,我们应该如何辨明数组中哪些分量未被使用或被删除?
关键点2:备用链表——将所有未使用或已删除的分量单独又链成一个链表。
插入时:从备用链表上取得“第一个”(不一定最靠前,而是 space[0].cur所指分量)结点作为代插入的新节点。
删除时:将被删除的结点转移到备用链表中。
初始化代码:
//!!!辨明数组中哪些分量未被使用:
// 创建一个备用链表(数组)表示未被使用的和被删除的分量
//!!!注意:
// 备用链表并非新建一个链表,而是将原链表中未被使用的和被删除的分量用游标链成一个链表
//!!!此函数应该当作初始化函数(整个都是备用链表)!!!
void InitSpace_SL(SLinkedList space){
for (int i = 0; i < MAXSIZE - 1; ++i) {
space[i].data = 0;//可省
space[i].cur = i + 1;
}
space[MAXSIZE - 1].data = 0;//可省
space[MAXSIZE - 1].cur = 0;
}
自定义一种 “malloc”:
注意每次调用都会更新 space[0].cur,即未使用或删除的分量位置(不一定最靠前)改变,便于下次使用。
//若备用空间链表非空,则返回节点下标,否则返回 NULL
//可用于生成头节点 或 分配新节点
//!!!此函数应用于循环中,每次更新 space[0].cur
int Malloc_SL(SLinkedList space){
int i = space[0].cur;
if (space[0].cur) {//备用空间链表非空
space[0].cur = space[i].cur;
}
return i;
}
自定义一种 “free”:
//将下标为 k 的空间回收到备用链表
void Free_SL(SLinkedList space, int k){
space[k].cur = space[0].cur;
//分析:
// space[0] 可视作备用链表头节点,space[0].cur 为整个链表中的一个未被利用/被删除的分量下标
// space[k] 为要删除的节点
// space[k].cur = space[0].cur; 让该节点的游标指向 space[0].cur 原来所指分量
space[0].cur = k;
//使备用链表头节点游标指向 space[k] ,让该节点融入备用链表
}
这种回收到备用链表的方法/思路有点像“从表尾到表头逆向建立单链表的算法”:
我们用动态单链表表示这种思路:
备用链表工作原理与此类似,将被删除的分量插在备用链表头节点后面。
这种方法能确保每次最先使用的是被删除的分量,而非之后未使用的分量。
从实例了解插入和删除:
现在要解决这样一个问题:
求(A - B)U (B - A)
文字描述:假设由终端输入集合元素,先建立表示集合 A 的静态链表 S ,而后在输入集合 B 的元素的同时查找 S 表,若存在和 B 相同的元素,则从 S 表中删除之,否则将元素插入 S 表。
思路:
首先初始化备用空间(备用链表),确定链表 S (A)的头节点。
因为是先创建 A 再从输入中把 B 的元素依次与 A 中每一项比较,所以我们要定好 A 的尾元素在哪里—— r 指向 S 的当前最后节点。
创建 A(尾插即可),依次读入 B 中元素,将每个元素与 A 中元素依次比较,相同从 A 中删除(此时要更改 r),不同则添加到 r 之后(注意不是扩充后的链表尾端,此时不要更改 r)
具体实现:
int difference(SLinkedList space){
InitSpace_SL(space);//初始化,创建链表和备用链表
int S = Malloc_SL(space);//因为刚刚初始化,此为生成头节点
int r = S;// r 用于指向 S 的当前最后节点
unsigned m, n;//记录 A 和 B 的个数
printf("Enter the size of A & B:\n");
scanf("%u %u", &m, &n);//获取 A 和 B 的个数
for (int j = 1; j <= m; ++j) {//建立 A 的链表
int i = Malloc_SL(space);//用 i 获取到被分配的空间下标
printf("Enter the No.%d in A:\n", j);
scanf("%d", &(space[i].data));
space[r].cur = i;//让 S 的当前最后节点游标指向刚被分配到的空间(插入到表尾)
r = i;//更新 r(最后节点变化)
}
space[r].cur = 0;//尾节点游标为 0 ,表示 A 读取结束
//此时 r 为链表 A 的尾节点
for (int j = 1; j <= n; ++j) {//依次读入 B 的元素
int b;//获取 B 中每一个元素的值
int p = S;// p 表示 k 的(逻辑上)前一个节点,便于删除操作
int k = space[S].cur;// space[S] 为链表 A 头节点,k 指向 A 的第一个节点
printf("Enter a number in B:\n");
scanf("%d", &b);
while (k != space[r].cur && space[k].data != b) {// k 在指向尾节点 或 找到 b 后退出 while
p = k;
k = space[k].cur;
}
if (k == space[r].cur) {//如果 k 是到了尾节点,表明 A 中没有该元素,执行插入
int i = Malloc_SL(space);
space[i].data = b;
space[i].cur = space[r].cur;
//意为:所有元素都插在 r 后面,而非扩充后的 A 链表尾端
//省去一个指向新链表尾端的游标
space[r].cur = i;//插入在 r 之后,注意 r 表示链表 A 的尾节点,不要更改
}else{//如果找到了 b ,执行删除
space[p].cur = space[k].cur;// p 为 k 的前一个节点,其游标指向 k 的下一个节点
Free_SL(space, k);
if (r == k) {//!!!注意:当删除的是尾节点时,更改 A 的尾节点( r )
r = p;
}
}
}
return S;
}
过程图解:
书中实例:A = (c,b,e,g,f,d) ; B = (a,b,n,f) ,求(A - B)U (B - A)
1)前四行代码执行后
注意创建 S 的头节点时用了一次 “malloc”,更新了备用链表的头节点游标
2)建立 A 的链表 S
3)修改过程:
在main()中测试
#include <stdio.h>
#include "StaticLinkedList.h"
int main(int argc, char *argv[]){
SLinkedList ls;//创建了一个结构数组
int head1 = difference(ls);
printf("The final list:\n");
show_LS(ls, head1);
return 0;
}
测试案例:
A = (2,1,6,7,4) ; B = (7,9,1) 求(A - B)U (B - A)
预期结果:2,6,4,9
答案正确