目录
前言
静态链表是一种比较古老的用法了,一般用在没有指针操作的传统语言上,对于我们日常工作的使用,是大概率用不上这个操作方式的。考虑到考试的需要以及对静态链表思想的把握,笔者还是将这一部分的内容纳入数据结构与算法专栏的学习内容当中,如果没有考试需求的读者,可以根据自身情况忽略该部分介绍。另外后面加入了腾讯的一道面试题,供大家一起学习!
专栏地址:数据结构与算法(c语言实例)_Felix Du的博客-CSDN博客
欢迎各位批判指正!
一、静态链表的引入
我们都知道,C语言是一个伟大的语言,他的魅力在于指针的灵活性,使得它可以非常容易地操作内存中的地址和数据,这比其他高级语言更加灵活方便。(面向对象使用对象引用机制间接地实现了指针的某些功能),但是早期的编程语言并没有C语言、JAVA,只有原始的Basic,Fortran等早期的编程语言,这些语言没有类似于C的指针功能,但是他们又想描述单链表,这要怎么来实现呢?
我们就不卖关子了,有人想出了用数组代替指针来描述单链表。这种用数组描述的链表我们叫做静态链表,这种描述方法叫做游标实现法。为什么叫它静态数组呢?原因很简单,因为我们声明的时候需要确定他的空间有多大(和数组一样),所以就说这是一个静态链表。
静态链表实例
我们需要注意两个很特殊的元素,第一个是下标为0的数据,一个是maxsize-1的数据,他们两个都是不存放东西的。
二、线性表的静态链表存储结构
上文我们了解到了静态链表的一些基本情况,那么下面我们将来研究线性表的链式存储结构。
cs
#define MAXSIZE 1000
typedef struct
{
ElemType data;//数据
int cur;//游标(Cursor)
}
Component,StaticLinkList[MAXSIZE];
除了第一个和最后一个之外,每一个元素的游标都是指向下一个数据。
对于静态链表的初始化相当于初始化数组,代码如下:
cs
Status InitList(StaticLinkList space)
{
int i;
for(i=0;i<MAXSIZE-1;i++)
space[i].cur = i + 1;
space[MAXSIZE-1].cur = 0;
return 1;
}
几个注意的点:
1)我们对数组的第一个和最后一个元素做特殊处理,他们的data不存放数据
2)我们通常把未使用的数组元素称为备用链表
3)数组的第一个元素,即下标为0的哪个元素的cur就存放备用链表的第一个结点的下标。
4)数组的最后一个元素,即下标为MAXSIZE-1的cur则存放在第一个有数值的元素下标,相当于单链表中的头结点作用。
那么这个时候可能大家会有疑问:这样子不还是挂羊头卖狗肉吗?这不还是数组,似乎没看出太多单链表的端倪。那么下面我们就从操作的角度来剖析一下,静态链表究竟是如何模拟单链表进行插入和删除的操作呢?
三、静态链表的插入操作
我们先来看一看静态链表是如何实现元素的插入的。静态链表中要解决的是:如何用静态模拟动态链表结构的存储空间分配,也就是需要的时候申请,不需要的时候释放。我们在前面的文章中提到过,在动态链表中,结点的申请和释放分别借用C语言中的malloc()和free()两个函数来实现。而在静态链表当中,我们操作的是数组,不存在像动态链表一样的结点申请和释放问题,所以我们需要自己实现这两个函数,才可以做到插入和删除操作。
而为了辨明数组中投哪些分量未被使用,解决的方法就是将所有未被使用过的及已被删除的分量用游标链成一个备用的链表。每次当我们进行插入时,便可以从备用链表上取得第一个结点作为待插入的新结点。我们可以结合下面的图片来进行理解,这里我们假设要在A的后面插入B这个元素:
这里是一个静态链表,第一个元素的游标存放的是备用链表的下标,,最后一个元素的游标存放的是第一个元素的下标。
插入操作的图解
我们要把B插入在C的前面,那么A就把下一个元素指向了B,将B的下标5作为了他的游标,然后B把自己的游标改成了他下一个元素C原本的下标2。那么当我们读取A之后要读取A的下一个元素,我们就会去访问到B了,那么B就成功地插入到A的后面了。
我们下面来看一下代码,代码由两部分组成:
首先是获得空间分量的下标:
cs
int Malloc_SLL(StaticLinkList space)
{
int i = space[0].cur;
if(sapce[0].cur)
space[0].cur = space[i].cur;
//把它的下一个分量用来作为备用
return i;
}
第一件事:把数组的第一个元素游标指向的下标给获取出来,将第一个元素指向它的下一个元素,插入之后就不是变成了空闲分量。代码如下:
cs
//在静态链表L中第i个元素之前插入新的数据元素
Status ListInsert(StaticLinkList L, int i,ElemType e)
{
int j,k,l;
k = MAXSIZE-1;//数组的最后一个元素
if(i<1 || i>ListLength(L)+1)
{
return ERROR;
}
j = Malloc_SLL(L);
if(j)
{
L[j].data = e;
for(l=1 ; l <=i-1 ;i++)
{
k = L[k].cur;
}
L[j].cur = L[k].cur;
L[k].cur = j;
return 1;
}
return ERROR;
}
四、静态链表的删除操作
那么我们都知道,有插入就会有删除,我们在B插入进来后,想要把C给删除,那么我们该怎么来做呢?
删除之前的静态链表
我们来分析一下:C离开了队伍,那么原本B的游标2指向的位置就没有元素了,那么这个时候我们需要顺位下来,将原本指向C的游标指向下一位D,那么B的游标就更改成3了,但是这个时候我们发现,B和C原本的游标都是3,这就产生了矛盾,所以我们除了处理B的游标,我们还要处理C原来的游标。我们将C归到备用链表中,那么C的游标就要指向备用链表6,然后原来6指向2,这样就把备用链表也给串起来了
删除之后的静态链表
代码操作如下:
cs
//删除在L中第i个数据元素
Status ListDelete(StaticLinkList L, int i)
{
int j,k;
if(i<1 || i>ListLength(L))
{
return ERROR;
}
k = MAXSIZE-1;
for(j = 1; j<=i-1;j++)
{
k = L[k].cur;//k1 = 1,k2 = 5
}
j = L[k].cur; //j = 2
L[k].cur = L(L,j);
return 1;
}
//将下标为k的空闲结点回收到备用链表
void Free_SLL(StaticLinkList space,int k)
{
space[k].cur = space[0].cur;
space[0].cur = k;
}
//返回L中数据元素个数
int ListLength(StaticLinkList L)
{
int j = 0;
int i = L[MAXSIZE-1].cur;
while(i)
{
i = L[i].cur;
j++;
}
return j;
}
五、静态链表的优缺点总结
1、优点
在插入和删除操作时,只需要修改游标,不需要移动元素,从而改进了在顺序存储结构中插入和移动操作需要移动大量元素的缺点。
2、缺点
没有解决连续存储分配(数组)带来的表长难以确定的问题。失去了顺序存储结构随机存储的特性。
3、小结
总的来说,静态链表其实是为了给没有指针的编程语言设计的一种实现单链表功能的方法。尽管我们可以用单链表就不用静态链表了,但这样的思考方式是非常巧妙地,应该理解其思想,以备不时之需。
六、单链表小结------Tecent面试题
【题目】快速找到未知长度和单链表的中间结点。
1、普通解法:
很简单,我们只需要遍历一遍单链表用来确定单链表的长度L。然后再次从头结点出发循环L/2次找到单链表的中间结点。这个算法的时间复杂度为
2、高级解法:
有一个很巧妙的方法:利用快慢指针来解决!原理操作如下,设置两个指针*search 和*mid都是指向单链表的头结点。其中*search指针的移动速度是*mid的两倍。当*search指向末尾结点的时候,mid就正好在中间了。这也是标尺的思想。具体的代码演示如下:
cs
Status GetMidNode(LinkList L,ElemType *e)
{
LinkList search,mid;
mid = search = L;
while(search->next != NULL)
{
//search移动的速度是mid的两倍
if(search -> next ->next != NULL)
{
serach = search -> next -> next;
mid = mid ->next;
}
else
{
search = search -> next;
}
}
*e = mid ->data;
return 1;
}
(本节完)
参考资料:
1、线性表9_哔哩哔哩_bilibili 鱼C小甲鱼