本文是《小甲鱼数据结构》的学习笔记,对视频课程中的相关知识进行总结。
欢迎大家在评论区多多留言互动~~~~
链表除了可以使用之前所说的方法实现,还可以通过数组经过游标实现法得到。
在上面的数组中,中间的是数据,上面的是游标,下面的是下标。我们可以看到整个链表的第一个位置和最后一个位置的数据是空的,也就是下表为0和下表为 999 所对应的数据的位置是空的。最后一个位置的游标指向的是第一个有数据的元素,而第一个位置的游标指向的是第一个数据为空的元素。也就是数组尾部的游标指向链表的头,数组首部的游标指向链表的尾。
有数据的部分可以看到游标为 2 的数据他的指针指向下标为 3 的数据;都是上一个的游标指向下一个的下表;而有数据的最后一个元素的游标是 0 。
表的静态链表存储结构如下所示
#defin MAXSIZE 1000
type struct
{
ElemType data; //数据
int cur; //游标(Cursor)
} Component, StaticLinkList[MAXSIZE];
所以静态链表是一个结构体,在这个结构体中我们可以看到它是由两部分组成的,一部分是数据,另一部分是游标。
静态链表初始化相当于对数组进行初始化,具体如下
Status InitList( StaticLinkList space)
{
int i;
for (i = 0; i < MAXSIZE-1; i++)
space[i].cur = i + 1;
space[MAXSIZE-1].cur=0;
return OK;
}
每个游标的值都取下表的下一个的值,最后一个游标指回 0 。
(1) 对数组的第一个和最后一个元素做特殊处理,它们的 data 不存放数据;
(2) 把未使用的数组元素称为备用链表;
(3) 数组的第一个元素,即下标为 0 的那个元素的 cur 就存放备用链表的第一个结点的下表;
(4) 数组的最后一个元素,即下标为 MAXSIZE - 1 的 cur 则存放第一个有数值的元素的下标。相当于单链表中的头结点的作用。
为了辨明数组中哪些分量未被使用,解决的办法是将所有未被使用过的以及已被删除的分量用游标链成一个备用链表。每当进行插入时,便可以从备用链上取得第一个结点作为带插入的新结点,如下图所示
实现该功能由两部分代码组成,首先是获得空闲分量下标
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur;
if (space[0].cur)
space[0].cur = space[i].cur; //把他的下一个分量用来作为备用。
return i;
}
Status ListInsert (StaticLinkList L, int i, ElemType e)
{
int j, k, l;
if (i < 1 || i > ListLength(L) + 1)
{
return ERROR;
}
j = Malloc_SLL(L);
if (j)
{
L[j].data = e;
for (l =1 =1; l <=i-1; l++)
{
k = L[k].cur;
}
L[j].cur = L[k].cur;
L[k].cur = j;
return OK;
}
return ERROR;
}
在这里我们以刚刚插入了数据的链表为例进行删除操作,具体来讲就是将下图中的 c 数据删除
经过删除操作之后得到的结果如下图所示
他做了两件事情,首先它将 C 的链接从含有数据的部分去除了,这部分体现在 B 的修改上,也就是在 C 前一个元素的修改上,而不是直接对 C 进行修改,具体的操作是将 B 的游标不是连在 C 上,而是连接在 D 的下标,这样跳过了 C 也就相当于将它从数据中删除了。第二步是将删除的部分放到备用链表中,具体地操作是将原先数据 C的上标对应 6,再将最开始的上标对应原先 C 的下标。
在这里可以发现一个规律,就是无论是在之前还是现在的操作中,无论是插入还是删除,这里用的方法都是从后向赋值替代,这样可以避免“从前到后替代”产生的覆盖作用。
/* 删除在 L 中的第 i 个数据元素*/
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if(i< 1 || i > ListLength(L))
{
return ERROR; //检测输入 i 的合法性
}
k = MAX_SIZE - 1; //最后一个元素,999
for (j =1; j<= i-1; j++)
{
k = L[k].cur; //经过循环把要删除的元素的 k 取出来;循环执行两次,执行第一次 k1 =1;执行第一次 k2 =5;
}
j = L[k].cur; //j = 2,得到的是 c 的上一个元素 b 的游标,即 c 的下标。
L[k].cur = L[j].cur; //L[k].cur = 3,将B的上标用 C 的上标替换,这样就跳过了 C ,相当于完成删除 C 第一步。
Free_SLL(L, j); //将要删除的元素释放掉
return OK;
}
/* 将下面标为 k 的空闲结点回收到备用链表 */
void Free_SLL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; //需要注意的是这里的 k 实际上是上面的 j ;这里是将 被删除元素的上标变为 6
space[0].cur = k; //再更新最开始那部分的上表
}
/* 返回 L 中的数据元素个数 */
int ListLength(StaticLinkList L)
{
int j = 0; // 用于计数
int i = L[MAX_SIZE-1].cur; // 数组中第一个数据的下标
while(i)
{
i = L[i].cur; //从数组中第一个非零的元素的下标开始,一个个地找下一个元素的下标,直至为0
j++;
}
return j;
}
分析上述删除功能中代码中的第一步里的循环,它实际上执行了两次循环,找到了元素 B 的下标,那么是否可以直接执行三次循环之后直接得到 B 的上表,答案是不行的。因为在后续的过程中有一个赋值操作L[k].cur = L[j].cur;
,如果只知道 B 的上表而不知道 B 的下标是无法进行这一项操作的,因为在静态链表中实际上是靠下标进行定位的,下标是不能缺少的。
优点在于插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序结构中的插入和删除操作需要移动大量元素的缺点。
缺点在于没有解决连续存储分配(数组)带来的表长难以确定的问题;失去了顺序存储结构随机存取的特性(即下标不再是连续的)。
总的来说,静态链表其实是为了给没有指针的编程语言设计的一种实现单链表功能的方法。尽管我们可以用单链表就不用静态链表了,但这样的思考方式是非常巧妙的,应该理解其思想,以备不时之需。
题目:快速找到未知长度单链表的中间结点。
分析:普通的方法很简单,首先遍历一遍单链表以确定单链表的长度 L 。然后再次从头结点出发循环 L2 次找到单链表的中间结点,算法复杂度为 O(L+L/2) = O(3L/2)。但是利用快慢指针原理可以将复杂度降低至 O(L/2)。设置两个指针 *search, *mid 都指向单链表的头结点。其中 *search 的移动速度是 *mid 两倍。当 *search 指向尾末结点的时候,*mid 正好就在中间了。这也是标尺的思想。
代码:具体代码如下
Status GetMidNode(StaticLinkList L, ElemType *e)
{
LinkList search, mid;
mid = serach = L;
while(search->next !=NULL)
{
// search 移动的速度是 mid 的2倍
if (search->next->next !=NULL)
{
search = search->next->next;
mid = mid->next;
}
else
{
search = search->next; //奇数选择中间那个,偶数选择靠前那个
}
}
*e = mid->data;
return OK;
}