上次介绍了单链表的实现。如果你认真的在计算机上敲出书上的算法并将其实现,你一定会有所收获的,对指针的使用也会熟练很多。
还是老规矩:
程序在码云上可以下载。
地址:https://git.oschina.net/601345138/DataStructureCLanguage.git
我们不得不说指针确实是个好东西,指针的存在大大增强了程序的灵活性,只要我们愿意,指针可以指向任何我们想要指向的已知的数据类型。但是早期的程序设计语言有些是不支持指针的(比如Basic和Fortran),那是不是在这些语言中就没法实现链表了呢?谁说用链表就必须要指针,指针仅仅是起一个存储地址的作用,那我们为什么不能在顺序存储结构中存放地址呢,只不过这个地址不用指针,只是用下标或位序(就是个int型整数)表示,虽然实际的存储用的是顺序存储,但是这样的结构却具备了链表的功能,这就达到了在没有指针概念的程序设计语言中实现链表的目的。
既然已经有办法在一段顺序的存储空间上实现链表数据结构,那具体该怎么做呢?
首先我们要明确,由于这些程序设计语言不支持指针概念,我们只能事先申请一段足够长的数组空间,使用它来存放和管理我们的链表,由于这段存储空间的大小在编译时就可以确定,而且数组的大小无法在程序运行过程中动态分配,所以我们把这样的结构称之为静态链表。
其次,由于我们的存储空间大小在编译的时候就已经确定并且无法在运行过程中改变,所以我们只能手工去管理这段内存空间,我们需要手工写代码实现内存空间的分配、释放和初始化。这里面就包含了很多的编程思想和编程技巧。
静态链表和单链表的不同之处不仅是存储结构上的差异,更重要的是:我们的静态链表可以在一段数组空间上同时存储和管理多个链表,但是单链表却无法从已有的存储空间上分出一段空间来存放第二个链表。
本次的程序中使用了大量的编程技巧。
1.手工写代码在一段连续的存储空间上完成内存的动态分配和释放以及存储单元的初始化,这本身就需要一些技巧。
2.要在一段数组上存储和管理多个链表,要保证链表各项操作的可用性和正确性,还要防止多个链表的操作相互影响。
我们想要实现内存的分配和回收就必须要引入备用链表,此时我们在一个数组中不仅要在多个数据链表中存储和维护我们的数据,还要管理好备用链表,我们的内存分配全靠它,他后面链接着很多的空闲结点,每次分配内存就要从这个备用链表上摘下一个节点,释放内存则要将回收的结点挂回到这个链表上。所以我们要在一个数组上搞出多个链表,有点像操作系统的存储器管理,学过操作系统的童鞋应该听过“空闲分区链”这个概念吧。
总而言之,上面那段话提到了两个重要概念:
备用链表(任何时候只有一个):就像一个管家一样管理大数组中暂时用不到的空闲结点。备用链表的的头结点约定就在数组的0号存储单元。
数据链表(只要还有足够的存储空间,理论上可以分配任意多个数据链表):存放我们的数据,所有数据结点都是从备用链表摘下来投入使用的,用完也必须被回收,挂回到备用链表。
接下来就要看看程序实现了:
//*******************************************引入头文件*********************************************
#include <stdio.h> //使用了标准库函数
//******************************************自定义符号常量*******************************************
#define DestoryList ClearList //DestoryList()和ClearList()的操作是一样的
#define OVERFLOW -2 //内存溢出错误常量
#define ILLEGAL -1 //非法操作错误常量
#define OK 1 //表示操作正确的常量
#define ERROR 0 //表示操作错误的常量
#define TRUE 1 //表示逻辑正确的常量
#define FALSE 0 //表示逻辑错误的常量
//******************************************自定义数据类型********************************************
typedef char ElemType; //元素类型
typedef int Status; //状态参量类型
//----------------线性表的静态单链表存储结构--------------------
#define MAXSIZE 1000 //链表的最大长度
typedef struct{
ElemType data; //数据域
int cur; //游标,作用相当于指针域
}component, SLinkList[MAXSIZE];
//***************************************线性静态单链表的主要操作******************************************
/*
函数:Malloc
参数:SLinkList L 静态链表首地址
返回值:新开辟结点的坐标
作用:(申请结点空间)若备用空间链表非空,从备用链表取出一个空闲结点
并返回该结点下标,否则返回0
*/
int Malloc(SLinkList L){
//L[0].cur记录了备用链表第一个空闲结点在数组中的位置
int i = L[0].cur;
//cur的值为0相当于指针域为NULL
//cur的值为0表示该节点后面没有后继,该节点是链表中的最后一个结点
//i的值不是0表示备用链表中还有空闲结点可以使用
if(i) { //if(i) <=> if(i != 0)
//备用链表的头结点指向原备用链表的第二个结点
//i(也就是L[0].cur)指示了第一个空闲结点的位置,
//所以L[i].cur指示了第二个空闲结点的位置。
//L[0]是备用链表的头结点,所以L[0].cur = L[i].cur;这句代码
//就是从备用链表把第一个空闲结点取走,然后把第二个空闲结点挂回到备用链表上
L[0].cur = L[i].cur;
}//if
//返回新开辟结点的坐标
return i;
}//Malloc
/*
函数:InitList
参数:SLinkList L 静态链表首地址
返回值:空表在数组中的位序
作用:构造一个空链表
*/
int InitList(SLinkList L){
//注意:此时一个数组中存储了两个链表:静态链表数据存储区域和备用链表
//静态链表的头结点在数组中的位置就是i,但是i不是0,因为0号单元已经规定
//就是备用链表的头结点。所以L[i].cur是静态链表首元结点。
//而备用链表的头结点是L[0],首元结点在数组中的位置是L[0].cur。
//由于一个数组中同时存储了两个链表,所以要区分开静态链表数据存储区域和
//备用链表,尤其要区分两者的头结点和首元结点的位置上的不同。
//而且要注意:由于静态链表数据存储链表的头结点是从备用链表上取出来的,位置不固定
//所以在调用的时候要用变量保存头结点在数组中所在的位置,否则后续操作将无法进行
int i;
//i存储了新开辟结点的坐标
i = Malloc(L);
//由于静态链表目前刚刚初始化,除了头结点一个数据节点都没有,所以要
//把静态链表头结点的指针域【也就是cur】设置为0,意思是后面没有后继结点
L[i].cur = 0;
//新开辟的第一个结点就是静态链表的头结点,头结点的位置为i
return i;
}//InitList
/*
函数:InitSpace
参数:SLinkList L 静态链表首地址
返回值:无
作用:(初始化备用空间)将一维数组L中各分量链成一个备用链表,
L[0].cur为头指针,"0"代表空指针
这个函数是用来初始化备用链表的,不是用来初始化静态链表的数据存储区域
*/
void InitSpace(SLinkList L) {
//此时静态链表还未投入使用,所以整个数组都是备用链表的空间
for(int i = 0; i < MAXSIZE - 1; ++i) {
//将第i+1个元素的下标(相对地址)存在第i个元素的cur中
L[i].cur = i + 1;
}//for
//最后一个元素的cur存放0,表示空指针,即该位置为备用链表表尾
L[MAXSIZE - 1].cur = 0;
}//InitSpace
/*
函数:ClearList
参数:SLinkList L 静态链表首地址
int n 存储数据链表表头结点在数组中的位序
返回值:无
作用:将线性表L置成空表,由于静态链表的大小在编译的时候就已经确定,
所以静态链表的内存不是动态分配的,也就不存在销毁这样的操作了。
由于清空操作造成的效果和销毁差不多,两者都可以清空已经存在的数据,
只不过清空不可以释放静态链表占用的内存空间,所以可以把清空操作
当成是静态链表的销毁操作。
*/
void ClearList(SLinkList L, int n){
int i = 0, j = 0, k = 0;
//先回收静态链表数据存储链表的头结点,因为这个节点也是从备用链表
//上面摘下来的,所以这个节点要先收回来。
//i指示存储数据链表首元结点的位置,由于是要回收头结点,所以要
//保存首元结点在数组中的位置,否则头结点被回收之后将无法继续
//回收后面的存储数据的结点。
i = L[n].cur;
//L[n].cur指示了数据链表头结点后面的首元结点在数组中的位置
//想要回收头结点就要把头结点和首元结点的连接切断,所以要
//将L[n].cur置为0,使头结点从数据链表上脱离下来。
L[n].cur = 0;
//k是备用链表第一个空闲结点在数组中的位置,k是起临时保存作用的
//回收链表之后还要把k指示的结点及其后面的链条挂回到备用链表的后面
//如果不保存k就会造成备用链表原有空闲结点丢失,在使用过程中可用空间无故变少。
k = L[0].cur;
//回收了数据链表头结点之后就要开始回收数据链表的首元结点以及后面的全部结点了。
//把数据链表的所有未被回收的结点连接到备用链表表头,这些节点将会被回收
L[0].cur = i;
//从数据链表的首元结点开始,向后回收每一个结点,直到数据链表表尾。
//如果某个结点的cur值为0,表示该结点没有后继,即该结点是链表的最后一个结点
//所以i != 0的意思是最后一个结点还没回收完。因为只有链表最后一个结点的cur为0。
while(i){ //while(i) <=> while(i != 0)
//j记录了被回收的结点在数组中的位置,最后一趟循环完成后,
//j指示的位置刚好是数据链表的尾元结点
j = i;
//i指向下一个元素
i = L[i].cur;
}//while
//备用链表在结点回收前后面就有一条空闲结点组成的链条,由于我们在回收数据链表
//结点的空间时已经重置了L[0].cur的值,所以旧的备用链表的空闲结点实际上已经和
//备用链表的头结点L[0]断开了连接,所以原先在备用链表中的头结点位置已经不被
//备用链表头结点记录了,如果我们不把它保存起来就会丢掉原有的空闲结点,导致
//静态链表的空闲结点在使用过程中越变越少,一部分空闲结点不受备用链表头结点管制
//且这些节点没法回收。所以备用链表的原来的首元结点在数组中的位置在回收前需要
//先保存在k变量中,只要找到数组中第k个位置,就可以顺着这个空闲结点找到后面所有的原先
//已经在备用链表中的空闲结点。现在我们要做的就是把新回收的空闲结点和原有的空闲
//结点合并到一个链中,统一被备用链表头结点的管理。具体做法就是直接把k指示的
//原有的空闲结点链条中的首元结点接到新的备用链表尾部,使它们连接在一起就可以了。
L[j].cur = k;
}//ClearList
/*
函数:Free
参数:SLinkList L 静态链表首地址
int k 被回收结点的在数组中的位置
返回值:无
作用:将下标为k的空闲结点回收到备用链表
*/
void Free(SLinkList L, int k){
//L[0].cur存储了备用链表第一个可用结点在数组中的位置
//L[k].cur = L[0].cur; 这行代码就是把原来的空闲结点组成的链条
//挂到这个将要回收的结点后面
L[k].cur = L[0].cur;
//待回收结点k就被加入到了备用链表中,成为了空闲结点链条中的第一个结点。
//(相当于在备用链表头部插入一个结点)。
L[0].cur = k;
}//Free
/*
函数:LocateElem
参数:SLinkList L 静态链表首地址
int n 静态链表头结点在数组中的位置
ElemType e 查找值为e的元素
返回值:如果找到第一个值为e的元素,返回查找到的元素在L中的位序,否则返回0
作用:在静态单链线性表L中查找第一个值为e的元素
*/
int LocateElem(SLinkList L, int n, ElemType e){
//i指向表中第一个结点
int i = L[n].cur;
//在表中顺链查找(若是字符串需要用strcmp()函数比较)
//while(i && L[i].data != e) <=> while(i != 0 && L[i].data != e)
while(i && L[i].data != e) {
//i指向下一个结点
i = L[i].cur;
}//while
//如果找到了值为e的结点,循环会提前终止,此时i存储的就是值为e的结点在数组中的位置
//如果没有找到,循环正常结束时i保存了尾元结点的cur,而尾元结点的cur值是0,