文章目录
静态链表:用一维数组实现的单链表(游标实现法,把游标当做指针来用)
这种链表主要是为了那些没有指针的高级语言设计的,用数组去实现单链表,即其本质是单链表,只不过不是C和C++那样使用结构体作为结点,使用指针直接指向下一个成员,而是用了数组来实现而已。所以他并没有数组的随机存取快的优势,但是却有数组长度不确定的缺点,但是有链表的插入删除方便的优点(毕竟本质是链表,没这优点也不能叫链表了),不需要像数组插入删除那样移动很多元素。所以虽然他是数组和链表的结合品,却并不能说是完全结合了二者的优势,只能说是确实具有二者的特点,具体说是具有链表的全部特点,却还有了数组的一点缺点。。。所以静态链表还是不如用指针实现的普通单链表啊。
一般C和C++肯定是不会用静态链表的,毕竟直接用指针和结构体配合就很好了,但是静态链表这种思路还是很值得学习的,没有什么事儿成不了,智慧能解决一切问题。
它就是先分配一个数组,由于不确定元素数目,所以只好申请大一点(这就是没有摆脱数组弊病的一大表现,数组长度未知在编程时真的是个大问题,所以一般都用动态数组vector类),然后每一个元素包括两个域,数据域和游标域(还是很像单链表对吧,平常链表的结点结构体也是两个元素,一个是数据,另一个是指针,这里是把指针换成了游标,游标是数组的下标,即一个整型数字)。
#define SIZE 1000//因为利用了数组,所以必须先设置长度,由于长度不确定所以只好设置大一点
typedef struct
{
ElemType data;
int cur;//cursor,游标
}Component, StaticLinkList[SIZE] //一次把结点(结构)和结点数组(结构数组)都定义好,后面代码中二者都可以被直接当做类型使用,StaticLinkList就表示一个链表
有的语言没有结构体,那就可以用一个并行数组来存储data和cur,即用两个同长度的数组来实现。比如C++有pair数组,不过C++也有结构,所以肯定还是用结构。
数组的第一个和最后一个元素有专门用途,并不存储数据(所以数组长度如果是N,则只有最多只可以存储N-2个元素),第一个元素的游标是数组中第一个可以使用的位置的下标,相当于备用链表(数组中还没被使用的空间)的头指针(这个游标相当于备用链表的头指针),而最后一个元素的游标是数组中第一个被使用的位置的下标,相当于头结点(头结点的指针域里的指针指向第一个存储数据的结点)。
即数组(起名为space,后面称呼方便)的第一个结点的游标是备用链表的头指针:指向备用链表的第一个可用结点,如果他是0,(space[0].cur == 0
),即备用链表的头指针指向自己所在的结点,就说明备用链表为空链表,没有存储空间了;
而最后一个结点相当于是数组中被使用了的链表的头结点,他的游标则是被使用链表的头指针,指向第一个存储了数据的结点,如果这个游标是0,(space[SIZE-1].cur == 0
),即指向第一个结点(如下图),但是第一个结点是不存储数据的特殊结点,所以就说明被使用链表为空链表,即没有没有任何数据被存在这个链表中。
初始化
静态链表的初始化需要把最后一个结点的游标设置为0,以表明被使用链表为空,而把第一个结点直到倒数第二个位置,每一个位置的游标都要设置为后继位置的数组下标。(上图就是被初始化的空链表的状态)
/*把一维数组space中的各个分量链成一个备用链表*/
/*space[0].cur是备用链表的头指针,为0则为空指针*/
Status InitLinkList(StaticLinkList space)
{
int i;
for (i = 0; i < SIZE - 1; ++i)
space[i].cur = i + 1;//用游标把所有结点链成一个备用链表
space[i].cur = 0;//最后一个结点
return OK;
}
经过总结我发现,数组的首结点和尾结点其实都是头结点,我们可以把静态链表使用的这个数组看作是一个备用链表和一个被使用链表,那么数组的首结点就是备用链表的头结点,其游标就是备用链表的头指针,为0则备用链表为空,没有可用位置了,整个数组的空间用完了;
数组的尾结点就是正被使用链表的头结点,其游标就是被使用链表的头指针,为0则表示没存东西,整个数组的空间都还没被使用。
被使用链表的结尾就是游标为0的普通结点,即不是数组首尾结点但是游标为0,如下图的6号位置,就是被使用链表的结尾。下面是求被使用链表的长度的函数,即存储数据的个数:
int ListLength(StaticLinkList L)
{
if (L[SIZE - 1].cur == 0)
return 0;
if (L[0].cur == 0)
return SIZE - 2;
int i = L[SIZE - 1].cur, ct = 0;
while (i = L[i].cur)
++ct;
return ct;
}
备用链表是真术语,被使用链表是我自己起的称呼,这样很便于我们去理解静态链表的实现原理
可以看出,这俩头指针是相悖的,所以绝对不可能同时为0。
插入:把数据插入到备用链表的第一个结点,不移动数组元素
静态链表的重点是用静态的数组结构去模拟动态的链表结构中存储空间的分配,普通单链表需要插入新结点就要去malloc或者new申请内存,删除结点则要free或者delete释放空间,是动态的操作内存。但是现在静态链表实际上内存位置已经有了,我们插入新结点只需要把新结点放到正确的位置就行,并不需要真的申请新内存,删除结点也是只需要把其它结点的游标重新设置一下,把被删除结点的位置放回备用链表就好,并不需要真的释放内存。所以我们要用静态的方式去模拟那种动态。
下面这个函数的作用就类似于malloc和new,负责分配位置
/*分配成功则返回位置下标,否则返回0*/
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur;//备用链表的第一个可用结点的下标
if (i)
space[0].cur = space[i].cur;//把备用链表的第二个结点设为备用链表第一个结点
return i;
}
位置有了,就该把数据搬进去了:
/*在L中第i个元素(i是说被使用链表的数据索引,不是数组的索引)前面插入数据e*/
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
if (i < 1 || i > ListLength(L) + 1)//先判断索引是否在正确范围,老是忘记,进入函数首先判断参数正确性
return ERROR;//ListLength(L)是被使用链表的长度,不是数组长度
int j = Malloc_SSL(L);
if (!j)
return ERROR;
L[j].data = e;//先把数据放在刚分配的位置
//然后去设置游标,把新结点放到第i个元素前面
int k = SIZE-1, q;
for (q = 1; q < i; ++q)
k = L[k].cur;//遍历链表,循环i-1次,则k是第i-1个链表元素所在的数组位置,L[k].cur就是第i个元素的数组位置
L[j].cur = L[k].cur;//让新元素的下一个元素是原来的第i个元素,插入到元素i前面了
L[k].cur = j;//原来第i-1个链表元素的游标指向第j个数组位置,即指向新插入的数据
return OK;
}
可见,新数据是直接放在备用链表第一个位置的,没啥麻烦的,麻烦的是要先把新结点的游标设置为原来第i-1个元素的游标,然后把被使用链表的第i-1个元素的游标重新设置为新结点的数组位置。
删除:把被删除结点从被使用链表中拿出,放回备用链表,不移动数组元素
/*把下标为k的结点回收到备用链表*/
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur;//把位置为k的待删除结点作为备用链表的第一个结点
space[0].cur = k;
}
/*删除L中第i个位置的数据元素e,此函数功能类似于free函数和delete*/
Status ListDelete(StaticLinkList L, int i)
{
if (i < 1 || i > ListLength(L) + 1)
return ERROR;
int k = SIZE - 1, j;
for (j = 1; j < i; ++j)
{
k = L[k].cur;//遍历链表,找到第i个链表元素的前一个链表元素所在的数组位置
}
j = L[k].cur;//第i个链表元素所在的数组位置
L[k].cur = L[j].cur;//从被使用链表中删除了结点i
Free_SSL(L, j);
return Ok;
}