前言:我们已经学习用C语言的结构体指针实现单链表,但是在一些语言中,比如早期的Basic、Fortran语言中是没有指针的,但是它们依然能用其他方法实现单链表
没有指针的计算机语言用数组来实现单链表,数组中的每个元素由两个数据域组成,data和cur。数据域data,用来存放数据元素,cur用来存放该元素的后继在数组中的下表。所以这种方法也叫游标实现法。
想象一下某人在洞窟里寻宝,脚下铺好了一片片青石板,一直延伸到宝藏的密室那里,但是贸然走过去就会中机关而粉身碎骨,因为这是有讲究的,第一步踩哪一片青石板,下一步又该踩哪一片…懂行的人会从踩第一片开始,就能发觉第一片指示的下一片,然后跳上去,再根据指示反复横跳,最后安全到达宝藏密室。
不要觉得一个术士在地板上反复横跳而不直接走过去很滑稽,你不懂。现在你开始学习静态链表,学完了你就懂了!
1.定义:我们把用数组描述的链表叫做静态链表
我们知道,声明一个指针的话,系统只分配了存储这个指针的4个字节空间,我们需要多大的空间通过malloc()函数申请,所以是动态的,而数组在声明后datatype array[size],系统就为它分配了size * datatype大小的空间,所以是静态的
鉴于C语言也支持数组,我们还是用C语言的数组来实现静态链表(其实是我也不会Basic、Fortran那些啦)
/*静态链表的C语言描述*/
#define MAXSIZE 1000 /*静态链表的最大长度*/
typedef char ElemType; /*假定静态链表中的数据为char型*/
typedef struct {
ElemType data;
int cur;
}SLinkList[MAXSIZE];
规定:我们开辟了长度1000的数组,作为静态链表的最大长度,其实是数据元素节点数最大只能是998,因为第一个和最后一个数组元素做特殊处理,不存储数据,而且我们把没有使用的数组元素链成备用链表
当我们需要插入一个元素的时候,就从备用链表上取下一个节点,放上数据,然后把它链接到静态链表上,同理,删除一个元素节点的数据后,就把它链接到备用链表上。
下表为0的数组空间的cur存放备用链表的第一个节点的下标;数组的最后一个元素的cur存放第一个有数值的元素的下标,相当于单链表中的头节点的作用,当整个静态链表为空时,也就是没有数组元素存放数据的情况下,最后一个元素的cur存放0
如上图,一个只有刘、关、张三个元素节点的静态链表,最后一个数据元素张之后没有其他数据元素,则cur置为0
2.静态链表各种操作的代码实现
2.1初始化静态链表
/*初始化静态链表,也就是说静态链表中还没有数据元素
将一维数组空间的各分量链成一个备用链表*/
Status InitSLinkList(SLinkList space) {
int i;
for (i = 0; i < MAXSIZE - 1; i++)
space[i].cur = i + 1;
/*space[0].cur = 1, 数组下标1的位置是备用链表的第一个节点*/
space[MAXSIZE - 1].cur = 0;
/*目前静态链表不含数据元素,最后一个数组元素的cur为0*/
return OK;
}
2.2返回静态链表中元素的个数
int ListLength(SLinkList L)
{
int i = 0;
int k = L[MAXSIZE - 1].cur;
while (k) {
/*L[MAXSIZE - 1].cur = 0表示数据元素链表为空,
不为零则等于第一个数据元素的下标,第一个数据元素节点的
cur则存储着第二个数据元素的下标...最后一个数据元素节点的cur为 0*/
++i;
k = space[k].cur;
}
return i;
}
2.3获取静态链表中的第i个元素值
Status GetElem(SLinkList L, int i)
{
int j;
int k = MAXSIZE - 1;
ElemType e;
if (i < 1 || i > ListLength(L))
return ERROR;
for (j = 0; j < i; j++)
k = L[k].cur;
e = L[k].data;
printf("链表中第i个元素值为:%c\n", e);
/*之所以加入这个打印语句是退而求其次,起码不违背用静态链表是因为没有指针的初衷
我还真见过在静态链表中使用指针的, 只能说那你闲的还用数组表示法干嘛!*/
return OK;
}
我们知道,在单链表中,我们在GetElem的形参列表中声明ElemType *e, 用e来接收元素值,因为在C语言中,值传递不会改变实参,在形参中声明指针变量,用地址传递可以改变实参,这里就有一个难点,静态链表的初衷就是因为一些计算机语言没有指针,那么用C语言模拟这种情况的话,在函数外声明的实参,在调用函数后它的值不会改变,所以干脆在函数内加入一条打印语句,调用函数后,直接把第i个元素的值打印出来
如果会BASIC和FORTRAN语言,会发现它们默认使用地址传递!
“如果实参是变量或者数组元素,那么在调用子程序时,采用地址传递方式,将实参变量或数组元素的地址传递给形参变量,使形参变量与对应的实参变量或数组元素共占用一个内存单元。调用之前,形参变量没有值。调用时,形参的值就是实参的值。调用完成之后对形参值的改变也会造成相应的实参值的改变”
———引自白海波等编著的《FORTRAN程序设计权威指南》机械工业出版社 2013年
学过一些汇编的同学应该知道,各种汇编指令,赋值等语句都是直接操纵CPU中的寄存器的,能通过地址总线、数据总线直接改变存储单元中的数值,我们知道,C语言作为一门更高级程序设计语言,用指针来代替对内存的直接操控,值传递的话函数中的形参数据是在另一片内存空间中对实参数据的拷贝,所以不能让实参和形参同步改变
2.4在静态链表中查找元素
/*在静态链表中查找第一个值为e的元素, 若找到则返回位序, 否则返回0*/
int LocateElem(SLinkList L, ElemType e) {
int i = L[MAXSIZE - 1].cur;
while (i && L[i].data != e) {
i = L[i].cur;
}
/*若 i = 0, 则不进入while循环,静态链表中没有数据元素*/
/*若L[i].data != e,而while循环结束,则i = 0,静态链表中没有等于e的数据元素
不然就是L[i].data == e,返回元素的位序 i*/
return i;
}
2.5遍历静态链表
void SLinkListTraverse(SLinkList L)
{
int i = L[MAXSIZE - 1].cur
while (i) {
printf("%c ", L[i].data);
i = L[i].cur;
}
printf("\n");
}
2.6静态链表的插入
我们知道,插入操作是从备用链表取下一个节点,放入数据元素,然后把它链接到静态链表上
第一步:如果备用链表不空,返回分配的节点下标,否则返回0
int Malloc_SL(SLinkList space) {
int i = space[0].cur;
/*space[0].cur是备用链表第一个节点的下标*/
if (space[0].cur)
space[0].cur = space[i].cur;
/*space[0].cur = 0说明静态链表满了,已经没有备用节点可以分配
space[0].cur = space[i].cur,是在节点分配后,让space[0].cur
保存下一个备用节点的下标,因为现在它已经变成备用链表的第一个节点了*/
return i;
}
第二步:将数据放入分配的节点中,然后把节点链入静态链表
Status SLinkListInsert(SLinkList L, int i, ElemType e)
/*这里 i 表示插入的位置, 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 = 0; l < i - 1; l++)
k = L[k].cur;
/*找到第i个元素前驱的位置*/
L[j].cur = L[k].cur;
L[k].cur = j;
/*改变游标,完成插入操作*/
return OK;
}
return ERROR;
}
2.7静态链表的删除
我们知道,删除静态链表的数据元素,先要将那个数据元素节点的值用一个变量返回(同样用打印代替),然后将这个节点放到备用链表中去
第一步:删除在L中的第i个数据元素
Status SLinkListDelete(SLinkList L, int i) {
int j;
int k = MAXSIZE - 1;
ElemType e;
if (i < 1 || i > ListLength(L))
return ERROR;
for (j = 0; j < i - 1; j++)
k = L[k].cur;
/*找到第i个元素前驱的位置*/
j = L[k].cur;
/*j为第i个元素的位置*/
e = L[j].data;
printf("删除的元素值为:%c\n", e);
L[k].cur = L[j].cur;
/*通过改变游标删除元素节点*/
Free_SL(L, j);
/*释放节点*/
return OK;
}
第二步:实现节点释放函数,即把要释放的节点放到备用链表中去
void Free_SL(SLinkList space, int k) {
space[k].cur = space[0].cur;
/*将下标为k的节点的cur存放备用链表第一个节点的下标
因为现在,下标为K的节点将成为备用链表的第一个节点*/
space[0].cur = k;
}
2.8静态链表的清空
/*清空自然是将所由的节点重新链接成备用链表,和初始化行为相似
真真没必要挨个删除节点再添加进备用链表,费劲*/
void ClearSLinkList(SLinkList L) {
int i;
for (i = 0; i < MAXSIZE - 1; i++)
space[i].cur = i + 1;
space[MAXSIZE - 1].cur = 0;
}
2.9静态链表的销毁
void DestroySLinkList(SLinkList L) {
/*数组内存由系统在栈空间分配,当函数执行完后,会被自动销毁
也就是最后main函数结束以后,静态链表自然被销毁了,不用自己实现*/
}
好了,静态链表的内容到此为止就结束了!讲真的,对于用C语言的程序员来说静态链表真没啥用,不过了解一下它的思想还是好的,万一哪天真就得用被Dijkstra大骂“脑残”的BASIC语言实现静态链表呢,记住迪杰斯特拉这个名字,以后的算法学习中会刻骨铭心!