引子
通常,我们习惯用结构体和指针来实现链表,每次创建节点都要使用 new 或 malloc 关键字,这一步非常慢,在删除节点时还要释放数据。在嵌入式系统中,往往数据是提前静态分配好的,我们只是需要以链表的形式来管理。这种情况下,静态链表是个不错的选择。
静态链表介绍
静态链表是这样一种数据结构,它通过数组来模拟链表的操作[用数组描述的链表]。每个节点包含一个数据元素和一个指向下一个节点的指针,其中指针不再是直接指向下一个节点的内存地址,而是指向下一个节点在数组中的下标。这样,静态链表就解决了动态链表频繁申请和释放内存的问题。
在静态链表的实现中,一般采用头节点作为链表的标识,即头节点不存储数据元素,仅用于标识链表的开端。链表中的每个节点都有一个下标,可以通过下标来访问对应的节点。
在静态链表的实现中,一般会预先申请一段连续的内存空间,这个内存空间被视为整个链表的空间,然后在这个空间中划分出若干个节点,每个节点都是一段连续的内存空间。静态链表的优点在于它可以通过预先申请一段连续的内存空间来实现,这样可以避免动态链表频繁申请和释放内存的问题。同时,在静态链表中,节点的访问是基于下标的,因此在访问链表中的任何一个节点时,可以直接通过下标来访问对应的节点,而不需要从头开始遍历整个链表。
静态链表的缺点在于它不支持动态扩容和缩容,因为在申请静态链表的内存空间时,需要预先确定内存空间的大小。同时,在静态链表中,节点的大小是固定的,因此无法存储大小不一的数据元素。静态链表的缺点在实时嵌入式系统中恰好不是缺点,因为嵌入式系统中,数据是可预测,提前预分配的,想要获得的是更好的性能。
动态链表和静态量表比较:
- 使用动态链表存储数据,无需预先申请内存空间,而是在需要的时候用 malloc 或 new 关键字进行申请,所以链表的长度没有限制。动态链表因为是动态申请内存的,所以每个节点的物理地址不连续,需要通过指针来顺序访问;
- 使用静态链表存储数据,需要预先申请足够大的一块内存空间,所以链表的初始长度一般是固定的。静态链表因为是用数组实现的,所以每个节点的物理地址连续。
对于动态链表,每个节点不仅存储了一个值,还存储了指向下一个节点的指针,在动态链表上进行移动也是用的指针。静态链表自然也需要具备这些特性,那如何用数组的方式进行实现呢?
静态链表实现
下面以单链表来举例:
不妨给链表中的节点从 0 开始编号,如果链表有 n nn 个节点,则编号依次为 0 , 1 , ⋯ , n − 1。每个节点的编号可以视为指向该节点的「指针」,于是用指针访问节点便成了用「索引」访问节点(因为索引就是从 0 开始的)。设链表可能达到的最大长度为 N,因此需要两个数组分别用来存储每个节点的值和指向下一个节点的指针【节点的值数组,节点的指针数组】:
#define MaxSize 1000
typedef struct
{
int Data; // 两个数据域
int Cur; // 一个是存储数据,另外一个就是游标(存储下一个元素的下标)
}Component,StaticList[MaxSize]; // Component 是备用链表,StaticList 是静态链表。
例如,对于下图中的(单)链表(红色为节点编号,黑色为节点存储的值)
为了使我们创建的空间能够得到充分的利用,我们还需要一条连接各个空闲位置的链表,方便我们的随取随用,这条链表也被称为备用链表。
备用链表的作用是回收数组中未使用或之前使用过(目前未使用)的存储空间,留待后期使用。也就是说,静态链表使用数组申请的物理空间中,存有两个链表,一条数据链表,另一条数组中未使用的空间,即备用链表。
此时,为了适应这个,会存在一个“潜规则”,默认,数组第一个元素即下表为 0 的的元素的 Cur 存放备用链表的第一个结点的下标(备用链表的表头),而数组最后一个元素的 Cur 存放第一个有数值的结点的下标(数据链表的表头)。也有的把数组第二个元素(Cur 为 1)用来作为数据链表的头节点。
或者
静态链表中设置备用链表的好处是,可以清楚地知道数组中是否有空闲位置,以便数据链表添加新数据时使用。
举个例子:
此例子使用数组最后一个元素的 Cur 存放第一个有数值的结点的下标(数据链表的表头)
#define MaxSize 1000
typedef struct
{
int Data; // 两个数据域
int Cur; // 一个是存储数据,另外一个就是游标(存储下一个元素的下标)
}Component,StaticList[MaxSize]; // Component 是备用链表,StaticList 是静态链表。
void Init(StaticList Space) // 静态链表初始化。主要有两点:1.将Cur游标存储下一个结点的下标
{ // 2.最后一个结点的Cur游标存储第一个有数值的元素的下标。
for(int i=0;i<MaxSize-1;i++) // space 是静态链表,用循环将第 i 个结点的Cur游标赋值为i+1。
{
Space[i].Cur=i+1;
}
Space[MaxSize-2].Cur=0; // 最后一个空闲的结点Cur置为0,相当于指针置为NULL。
Space[MaxSize-1].Cur=0; // 最后将最后一个结点的Cur游标初始化为0。先开始是空表所以为0,如果
// 不是空表,那么此处就会记录第一个有数值元素的下标。
}
数据结构与算法(三)——线性表(下)静态链表篇_无条件j的博客-CSDN博客
静态链表(C语言)详解 - 哔哩哔哩 (bilibili.com)
管理上可以使用多个链表管理静态链表:
1. free pool 放置所有的数据结点,简单说就是把数组使用下标链接起来。
2. allocated pool,把已经分配的节点连接到此链表下。分配从 free pool 分配,释放时返回给 free pool。
这样一个静态数据集,可以由多个链表来管理【一个数组(资源池)可以根据使用属性生成若干个链表】。
对于嵌入式系统来说,还可以数据节点和游标(指针)结点分离。
比如每个结点包括两个内容,结点的值和下一个结点的地址,如果我们用数组来表示的话就是:
- 结点的值数组
[N]
- 结点的指针数组
[N]
结点指针数组可以使用多个,这样就完成了对同一数据的多个维度的管理,下面随便列举一些用途的例子。
a. free pool 静态链表,维护尚未使用的结点值数链表。
b.used pool 静态链表,维护已经分配的结点值数链表。
c. 升序 静态链表,维护按照升序排列的结点数值链表。
d.降序 静态链表,
e. 按照权重维护的静态链表。
f.等等。
SL_initialize() //
SL_add2head(*sl, id)
SL_add2tail(*sl, id)
SL_remove(*sl, id)
SL_getcount(*sl)
SL_isempty(*sl)
SL_getfirst(*sl)
SL_getlast(*sl)
SL_getnext(*sl)
SL_getprev(*sl)
SL_tranverse(*sl)