很多学数据结构的朋友一提到链表,第一反应就是 struct Node*,然后 malloc、free 走起。但在算法竞赛、嵌入式开发、操作系统内核这类对运行效率极其敏感的场景里,真正的链表实现往往不是那个样子,而是一排预先分配好的数组,加几个整数下标在充当“指针”。这种写法在教材里叫静态链表,做工程的人通常叫它数组模拟链表。
它解决的核心问题很明确——把链式结构落进连续内存里,让节点的分配和回收变得极其可控。这篇文章不复制教材定义,只讲实操时真正需要盯住的那些细节:空闲表怎么建、插入时谁先谁后、删除后节点怎么回收、遍历的停止条件为什么总在出 bug。不管你是正在备考数据结构,还是写邻接表存图,或者打算用定长内存池代替频繁 malloc,这些东西应该都用得上。
1. 为什么放着动态链表不用,非要用数组模拟
1.1 数组模拟链表的底层逻辑:用整数下标代替真实指针
链表的核心是什么?是一堆节点,每个节点除了存数据,还要存一个“下个节点在哪”的信息。正常做法是让节点里存一个真实指针,指向堆内存里某个对象。数组模拟链表换了个思路:所有节点提前申请好,放进一个连续数组里,节点里存的不再是地址,而是数组下标。
比如定义一个节点池:
c复制#define MAXN 100000
int data[MAXN]; // 节点数据域
int nxt[MAXN]; // 节点“指针”域,存的是下一个节点的下标
int head; // 链表的头节点下标
这个 nxt 数组看起来是个 int,实际上语义就是链表里的 next 指针。head 也不再是地址,而是头节点在数组里的位置。当 nxt[p] = -1,就表示 p 节点后面的链断了,也就是链表到了尾端。
这里 -1 就是模拟了空指针 NULL。为什么用 -1 而不是 0?因为数组下标从 0 开始,0 本身可能是一个有效节点,拿 0 当空指针,等于白白牺牲掉第一个槽位,而且判断条件容易和“下标为 0 的节点”混在一起。我见过不少初学代码在 while(p++) 和 p != -1 之间来回横跳,最后定位到根因,基本都是这个约定没统一。
1.2 优缺点差距:省时省内存,但不省心
数组模拟链表最直接的优势有三个:分配快、缓存友好、无内存碎片。动态链表每创建一个节点都要走一次 malloc,每删除一个节点还要 free,频繁调用会有不小的系统开销,时间长了还有内存碎片问题。数组模拟就是在初始化时把所有槽位管理好,要节点时从空闲表拿一个下标,不要了再还回去,整个过程就是几次整数赋值,常数时间。
用一张表看两者的差异更清楚:
| 对比维度 | 动态链表 | 数组模拟链表 |
|---|---|---|
| 节点分配 | 每次 malloc,开销大 | 从空闲表取下标,O(1) 常数时间 |
| 节点回收 | free,可能产生碎片 | 归还到空闲表,O(1) |
| 内存连续性 | 不连续,缓存命中率一般 | 连续数组,遍历时缓存较友好 |
| 容量弹性 | 可一直申请到堆内存耗尽 | 初始化定死,满了就是满了 |
| 调试难度 | gdb 里看指针地址 | 实际是下标,需要打印 data 数组 |
代价也很明显:数组长度是写死的。如果你预估 N 不够大,链表满了之后再想插新节点,没有任何办法。教科书里讲它“省内存”,很多人误以为它能动态伸缩——它省的是内存碎片和管理开销,不是省容量。
1.3 什么场景我建议直接上数组模拟
不是所有链表都值得用数组模拟。我自己的判断标准很简单:如果链表的数量、长度在写代码前能估出上界,并且对插入删除性能有要求,就用数组模拟。最典型的是图论里的邻接表,每条边就是一个链表节点,总共多少条边是确定的,提前开一个 to[MAXM] + nxt[MAXM] 就完事。还有哈希表拉链法,每个桶都是一条链表,节点总数不会超过插入次数,也特别适合数组模拟。
反过来,如果链表节点增减极其剧烈,完全估不准规模,或者你只是写业务代码不追求极限性能,老老实实用动态链表和标准库容器就是更稳的选择。数组模拟不适合任何“大得没边”的场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 初始化阶段:空闲表建不好,后面全白搞
2.1 数据结构定义:两条平行数组还是结构体数组
两种写法都能用。第一种就是刚才那种 data[] 和 nxt[] 两条平行数组。第二种是把节点定义成一个结构体,然后用结构体数组充当节点池:
c复制struct Node {
int data;
int nxt;
};
struct Node pool[MAXN];
两种写法在逻辑上完全等价。平行数组的好处是访问节点数据时少一层点操作,读取起来 data[p] 很直接,代码在编译器眼里也没什么区别。结构体数组的好处是语义更聚合,一个节点该有的内容都在一起,写复杂操作时不会被数组名绕晕。
