本文面向刚入门数据结构、已掌握动态链表但看不懂静态链表的新手,全程从已知到未知,循序渐进拆解所有核心知识点、代码逻辑和新手高频误区,看完就能彻底吃透静态链表。
目录
- 什么是静态链表?和动态链表的核心区别
- 静态链表的核心规则(新手必记)
- 静态链表核心函数逐行精讲
- 新手最容易踩的 8 个误区 & 疑问全解答
- 静态链表完整可运行示例
- 核心知识点总结
一、先搞懂:什么是静态链表?和动态链表有什么区别?
我们最开始都是先学会了动态链表,再接触静态链表,所以已经掌握的知识入手,拉齐认知。
1.1 回顾动态链表
动态链表的每个结点,是通过
malloc在堆上动态分配的,结构体和核心逻辑如下:// 动态链表结点结构 typedef struct Node { int data; // 存放有效数据 struct Node *next; // 指针,存下一个结点的内存地址 } Node;
- 核心逻辑:用
next指针存下一个结点的内存地址,把结点串成链表- 空指针:用
NULL表示 “没有下一个结点”,即链表结尾- 内存管理:结点用
malloc动态申请,free动态释放
1.2 静态链表的定义与核心设计思想
静态链表,本质就是用数组来模拟链表的结构,也叫数组模拟链表。它的出现,是为了解决早期编程语言没有指针(如早期 BASIC)无法实现链表的问题;现在学习它,是为了掌握「用下标模拟指针」的设计思想,这在内存池、缓存设计、嵌入式开发等场景中应用广泛。
静态链表的核心设计:
- 提前申请一块固定大小的数组,作为整个链表的「内存池」,数组的每个元素就是一个链表结点
- 不用真实的内存指针,而是用数组下标来模拟
next指针,这个「伪指针」我们叫它cur(游标)- 数组里的结点分为两类:一类存有效数据(叫数据链表),另一类是未使用的空闲结点(叫备用链表 / 空闲结点池)
1.3 静态链表的结构体定义
静态链表的结点结构体,国内教材通用的标准写法如下:
// 静态链表的最大容量,即数组的总长度 #define MAXSIZE 100 // 静态链表结点结构 typedef struct { int data; // 存放有效数据,和动态链表的data完全一致 int cur; // 游标,存下一个结点的数组下标,替代动态链表的next指针 } SNode, SLinkList[MAXSIZE];其中
SLinkList[MAXSIZE],就是定义了一个长度为MAXSIZE的数组,每个元素都是SNode类型的结点,也就是我们的整个「内存池」space。
二、静态链表的核心规则(新手必记)
这是你看懂所有代码、避开所有误区的基础,必须先记牢。
2.1 cur 的本质:用数组下标模拟 next 指针
cur变量里存的,永远是下一个结点在数组里的下标,而不是数值本身!举个最直观的例子:
- 代码
space[1].cur = 3;的含义是:下标为 1 的结点,它的下一个结点,是数组里下标为 3 的元素(也就是space[3])- 它完全等价于动态链表的
p->next = q;(p 指向 1 号结点,q 指向 3 号结点)额外说明:数组下标从 0 开始计数,因此:
- 数组的 0 号位置 = 下标为 0 的元素 =
space[0]- 数组的 3 号位置 = 下标为 3 的元素 =
space[3]- 以此类推,完全对应,无任何偏移。
2.2 空指针的约定:不同教材的差异说明
动态链表用
NULL表示空指针,静态链表则用一个特殊数字表示 “没有下一个结点”,不同教材 / 代码的约定不同,但核心逻辑完全一致,只要同一套代码内约定统一即可:表格
约定类型 空指针表示 适用场景 教材通用版(严蔚敏《数据结构》) 0 国内高校最常用的经典约定,0 号结点不存数据, cur=0代表链表结尾通用简化版 -1 避免和 0 号下标冲突,新手更容易理解 特殊标记版 -2 专门用 cur=-2标记「该结点是空闲结点」,方便快速判断结点状态本文全程采用严蔚敏教材的经典约定:
- 用
0表示空指针(对应动态链表的NULL)- 0 号结点不存储有效数据,专门作为备用链表的头结点
2.3 核心设计:双链表共用同一数组 space
这是新手最困惑的问题:数据链表和备用链表不是两个链表吗?为什么都用同一个
space数组?核心逻辑:
space数组就是一块提前申请好的「内存池」,数据链表和备用链表,只是这块内存池里两种不同角色的结点,它们共用同一块数组空间,通过cur指针分别串成两条完全独立的链表。我们给两条链表做明确的职责划分:
(1)备用链表(空闲结点池)
- 作用:管理数组里所有未被使用的空闲结点,等价于动态链表的「堆内存」
- 头指针:固定为
space[0].cur,永远指向备用链表的第一个空闲结点- 结尾:用
cur=0表示备用链表为空,无可用空闲结点- 核心操作:
Malloc_SL(分配结点)从这里取结点,Free_SL(回收结点)把不用的结点还回来(2)数据链表
- 作用:存储有效数据,功能和动态链表完全一致
- 头指针:必须单独用一个变量保存(比如
int data_head;),绝对不能用space[0].cur- 结尾:用
cur=0表示数据链表的结尾- 核心操作:插入、删除、遍历,逻辑和动态链表完全一致,仅把
next指针替换为cur下标(3)0 号结点的专属职责
必须明确:0 号结点永远不存储有效数据,它的唯一作用,就是作为备用链表的头结点,管理整个空闲结点池。
这也是数据链表的头结点不能是 0 号的核心原因:0 号结点已经被备用链表占用,它的
cur永远指向备用链表的第一个空闲结点,和数据链表没有任何关系。
三、静态链表核心函数逐行精讲(新手必看)
这一部分拆解最开始可能看不懂的 3 个核心函数,每一行都配逻辑类比和状态模拟,看完就能彻底懂。
3.1 初始化函数
InitSpace_SL:把数组连成备用链表函数作用:把整个
space数组的所有结点,通过cur指针串成一条备用链表,完成静态链表的初始化,提前准备好完整的空闲结点池。// 初始化静态链表的备用链表 void InitSpace_SL(SLinkList &space) { // 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针 // "0"表示空指针 for (int i = 0; i < MAXSIZE - 1; ++i) { space[i].cur = i + 1; } space[MAXSIZE - 1].cur = 0; }逐行拆解
for 循环:
space[i].cur = i + 1;让数组里的第i个结点,指向第i+1个结点,完成链表的串联:
i=0时:space[0].cur = 1→ 0 号结点指向 1 号结点i=1时:space[1].cur = 2→ 1 号结点指向 2 号结点- ...
- 直到
i=MAXSIZE-2时:space[MAXSIZE-2].cur = MAXSIZE-1→ 倒数第二个结点指向最后一个结点
space[MAXSIZE - 1].cur = 0;让数组最后一个结点的cur=0,即指向空指针,作为备用链表的结尾。初始化后的状态模拟(以
MAXSIZE=6为例)表格
数组下标 i space[i].cur 所属链表 说明 0 1 备用链表头 指向备用链表的第一个结点 1 号 1 2 备用链表 空闲结点 2 3 备用链表 空闲结点 3 4 备用链表 空闲结点 4 5 备用链表 空闲结点 5 0 备用链表尾 链表结尾,指向空指针 0 此时备用链表链路:
0 → 1 → 2 → 3 → 4 → 5 → 0,数据链表为空。
3.2 结点分配函数
Malloc_SL:从备用链表取空闲结点对应动态链表的
malloc函数,作用是从备用链表中取出一个空闲结点,返回它的下标,给数据链表使用。// 分配结点:若备用空间链表非空,则返回分配的结点下标,否则返回0 int Malloc_SL(SLinkList &space) { // 步骤1:取出备用链表的第一个空闲结点的下标 int i = space[0].cur; // 步骤2:如果备用链表非空(还有空闲结点) if (space[0].cur) { // 步骤3:把备用链表的头指针,后移到下一个空闲结点 space[0].cur = space[i].cur; } // 步骤4:返回分配到的结点下标 return i; }逻辑类比 + 逐行拆解
这个函数的本质,就是从单链表的头部删除一个结点,这个单链表就是备用链表。用你熟悉的动态链表类比,瞬间就能懂:
- 动态链表:删除头结点 → 先存下头结点地址 → 把头指针移到头结点的 next → 返回原头结点地址
- 静态链表:分配空闲结点 → 先存下第一个空闲结点的下标
i→ 把备用链表头指针space[0].cur移到i的下一个结点 → 返回i分配过程模拟(基于上面的初始化状态)
我们执行
Malloc_SL(space)分配第一个结点:
- 步骤 1:
i = space[0].cur→i=1- 步骤 2:
space[0].cur=1≠0,进入 if 判断- 步骤 3:
space[0].cur = space[1].cur→space[0].cur=2- 步骤 4:返回
i=1分配后的状态表格:
数组下标 i space[i].cur 所属链表 说明 0 2 备用链表头 现在指向备用链表的第一个结点 2 号 1 待设置 数据链表 已被分配,可用来存储数据 2 3 备用链表 空闲结点 3 4 备用链表 空闲结点 4 5 备用链表 空闲结点 5 0 备用链表尾 链表结尾 此时备用链表链路:
0 → 2 → 3 → 4 → 5 → 0,数据链表已分配 1 号结点,我们可以用data_head=1记录数据链表的头。
3.3 结点回收函数
Free_SL:把不用的结点还回备用链表对应动态链表的
free函数,作用是把数据链表中不再使用的结点,回收到备用链表中,让它重新变成可分配的空闲结点。// 回收结点:将下标为k的空闲结点回收到备用链表 void Free_SL(SLinkList &space, int k) { // 步骤1:让被回收的结点k,指向当前备用链表的第一个结点 space[k].cur = space[0].cur; // 步骤2:让备用链表的头指针,指向被回收的结点k space[0].cur = k; }逻辑类比 + 逐行拆解
这个函数的本质,就是在单链表的头部插入一个结点,这个单链表还是备用链表。继续用动态链表类比:
- 动态链表:头插新结点 → 让新结点的 next 指向原头结点 → 把头指针指向新结点
- 静态链表:回收结点 k → 让 k 的 cur 指向原备用链表的第一个结点
space[0].cur→ 把备用链表头指针space[0].cur指向 k回收过程模拟(基于上面的分配状态)
现在我们回收 1 号结点,执行
Free_SL(space, 1):
- 步骤 1:
space[1].cur = space[0].cur→space[1].cur=2- 步骤 2:
space[0].cur = 1→space[0].cur=1回收后的状态表格:
数组下标 i space[i].cur 所属链表 说明 0 1 备用链表头 现在指向备用链表的第一个结点 1 号 1 2 备用链表 已被回收,变回空闲结点 2 3 备用链表 空闲结点 3 4 备用链表 空闲结点 4 5 备用链表 空闲结点 5 0 备用链表尾 链表结尾 此时备用链表链路回到初始化状态:
0 → 1 → 2 → 3 → 4 → 5 → 0,1 号结点被成功回收。
四、新手最容易踩的 8 个误区 & 疑问全解答
这里汇总了入门阶段的所有核心疑问,一次性彻底解决。
误区 1:数据链表和备用链表是两个独立的数组?
错误!它们是共用同一个
space数组的两条独立链表,只是数组里的结点分为两种角色:
- 被数据链表的
cur串起来的结点,就是数据结点,用来存有效数据- 被备用链表的
cur串起来的结点,就是空闲结点,用来等待分配它们的区别仅在于cur指向的链路不同,本质都是space数组里的元素。
误区 2:0 号结点是数据链表的头结点?
错误!0 号结点的唯一职责是作为备用链表的头结点,永远不存储有效数据。数据链表的头结点,必须用一个单独的变量(比如
int data_head)来保存,和 0 号结点没有任何关系。
误区 3:可以通过
space[0].cur找到数据链表的第一个结点?完全错误!这是新手最核心的误区!
space[0].cur永远只指向备用链表的第一个空闲结点,和数据链表没有任何关系。举个例子:
- 你分配了 1 号结点作为数据链表的头,那么
data_head=1- 此时
space[0].cur=2,指向的是备用链表的第一个结点 2 号,和数据链表的头 1 号完全无关你永远只能通过自己定义的data_head变量,找到数据链表的第一个结点。
误区 4:备用链表的结点一定是连续的?下标 1 一定是备用链表的开始?
错误!
- 初始化时,备用链表是连续的:
0→1→2→3→4→5→0,此时下标 1 是备用链表的开始- 但执行多次分配和回收后,备用链表的顺序会被完全打乱,变成不连续的比如:你先分配 1、2、3 号结点,再回收 2 号结点,此时备用链表的链路是
0→2→4→5→0,备用链表的第一个结点是 2 号,而非 1 号,结点也不是连续的。
误区 5:
space[i].cur = 3,是指向数值 3?错误!
space[i].cur = 3的含义是:让 i 号结点,指向数组里下标为 3 的那个结点(space[3]),而非指向数值 3 本身。cur里存的永远是数组的下标,也就是下一个结点的「位置」,不是数据值。
误区 6:不同教材的空指针约定不一样,哪个是对的?
没有对错之分,只是约定不同,核心逻辑完全一致。不管是用 0、-1 还是 - 2 表示空指针,本质都是用一个特殊数字表示 “没有下一个结点”。只要你在同一套代码里保持约定统一,就不会有任何问题。新手入门建议先掌握严蔚敏教材的经典约定(
cur=0表示空),这是国内数据结构课程、考试最常用的版本。
疑问 7:怎么区分数组里的结点是数据结点还是空闲结点?
有两种通用判断方法,根据你的实现约定选择:
- 经典方法(严蔚敏教材):看结点是否在备用链表的
cur链路上
- 如果一个结点,从
space[0].cur出发顺着cur能找到,那它就是空闲结点- 如果一个结点,不在备用链表链路里,但从
data_head出发顺着cur能找到,那它就是被使用的数据结点- 标记方法:用特殊的
cur值标记空闲结点比如约定cur=-2表示该结点是空闲结点,只要结点的cur=-2就是空闲的,否则就是被使用的。这种方法判断更简单,但会占用一个特殊的cur值。
疑问 8:静态链表到底有什么用?为什么不直接用动态链表?
静态链表的核心优势和不可替代的使用场景:
- 无指针环境: 早期无指针的编程语言,只能用数组模拟链表
- 固定内存池: 提前申请固定大小的数组,不会出现动态内存分配的内存碎片问题,在嵌入式、单片机等内存受限的场景中广泛应用
- 高性能: 结点的分配和回收都是 O (1) 时间复杂度,比
malloc/free快很多,在高频分配回收的场景中优势明显- 持久化存储: 静态链表的数组可以直接写入磁盘,不需要处理指针的序列化问题,在简单的持久化场景中非常实用
五、静态链表完整可运行示例
下面是一套完整可直接运行的代码,包含静态链表的初始化、结点分配、尾插法插入、遍历、回收全流程,可以直接运行观察每一步的状态变化。
#include <stdio.h>
// 静态链表最大容量
#define MAXSIZE 10
// 静态链表结点结构
typedef struct {
int data;
int cur;
} SNode, SLinkList[MAXSIZE];
// 1. 初始化备用链表
void InitSpace_SL(SLinkList space) {
for (int i = 0; i < MAXSIZE - 1; ++i) {
space[i].cur = i + 1;
}
space[MAXSIZE - 1].cur = 0;
}
// 2. 分配结点
int Malloc_SL(SLinkList space) {
int i = space[0].cur;
if (space[0].cur) {
space[0].cur = space[i].cur;
}
return i;
}
// 3. 回收结点
void Free_SL(SLinkList space, int k) {
space[k].cur = space[0].cur;
space[0].cur = k;
}
// 4. 数据链表:尾插法插入元素
int ListInsert_Tail(SLinkList space, int *data_head, int val) {
// 分配新结点
int new_node = Malloc_SL(space);
if (new_node == 0) {
printf("备用链表为空,分配失败!\n");
return -1;
}
// 给新结点赋值
space[new_node].data = val;
space[new_node].cur = 0; // 新结点作为尾结点,cur=0
// 如果数据链表为空,新结点作为头结点
if (*data_head == 0) {
*data_head = new_node;
return 0;
}
// 找到数据链表的尾结点
int p = *data_head;
while (space[p].cur != 0) {
p = space[p].cur;
}
// 把新结点插入到尾部
space[p].cur = new_node;
return 0;
}
// 5. 遍历数据链表
void ListTraverse(SLinkList space, int data_head) {
if (data_head == 0) {
printf("数据链表为空!\n");
return;
}
int p = data_head;
printf("数据链表元素:");
while (p != 0) {
printf("%d ", space[p].data);
p = space[p].cur;
}
printf("\n");
}
// 主函数:测试静态链表
int main() {
SLinkList space;
int data_head = 0; // 数据链表的头指针,初始为空(0)
// 1. 初始化静态链表
InitSpace_SL(space);
printf("=== 静态链表初始化完成 ===\n");
// 2. 插入元素
ListInsert_Tail(space, &data_head, 10);
ListInsert_Tail(space, &data_head, 20);
ListInsert_Tail(space, &data_head, 30);
printf("=== 插入元素10、20、30完成 ===\n");
// 3. 遍历数据链表
ListTraverse(space, data_head);
// 4. 查看备用链表的头结点
printf("当前备用链表的第一个空闲结点下标:%d\n", space[0].cur);
return 0;
}
运行结果
=== 静态链表初始化完成 ===
=== 插入元素10、20、30完成 ===
数据链表元素:10 20 30
当前备用链表的第一个空闲结点下标:4
代码核心说明
- 这里的
data_head就是我们单独定义的、用来保存数据链表头结点下标的变量,所有对数据链表的操作,都要通过这个变量进行space[0].cur只用来管理备用链表,和数据链表完全无关- 整个操作逻辑和动态链表完全一致,仅把
next指针替换为cur下标
六、总结:静态链表核心知识点一句话记忆
- 核心本质: 静态链表就是用数组模拟链表,用数组下标
cur模拟next指针- 双链表设计: 同一个
space数组里,有两条独立的链表:存数据的数据链表、管空闲的备用链表- 0 号结点职责: 永远是备用链表的头结点,不存有效数据,
space[0].cur永远指向备用链表的第一个空闲结点- 数据链表头: 必须单独用变量保存,和 0 号结点无关
- 核心操作: 初始化是把数组连成备用链表;分配是从备用链表头部删结点;回收是往备用链表头部插结点
- 空指针约定: 经典约定用 0 表示空指针,不同教材约定不同,核心逻辑完全一致
如果这篇文章帮你彻底搞懂了静态链表,欢迎点赞、收藏、关注;如果还有任何疑问,欢迎在评论区留言,我会一一解答。
:从原理到代码,新手全疑问一次性解决&spm=1001.2101.3001.5002&articleId=159581878&d=1&t=3&u=ab997dfb8a70422ab1614556e4089632)
1万+

被折叠的 条评论
为什么被折叠?



