数据结构第 2 章:线性表——顺序表与链表的取舍

写于 2026-11-01

线性表是这门课第一个真正要写代码的结构。它的逻辑关系简单到一句话就能说完(一对一、有唯一首尾),但同一个逻辑结构用两种存储方式实现后,性能差异巨大——这一章真正要学的是"什么时候选哪个",而不是"两个都会写"。

一、线性表的定义

n 个数据元素的有限序列,n ≥ 0。除第一个元素外每个元素有且仅有一个直接前驱,除最后一个外有且仅有一个直接后继。

注意三个关键词:有限(元素个数确定)、序列(有顺序,元素有位置)、同类型(每个元素属于同一数据对象)。

二、顺序表:地址连续带来的能力与代价

顺序表用一组地址连续的存储单元依次存放元素,逻辑相邻即物理相邻。

设每个元素占 L 个存储单元,首地址为 LOC(a₁),则第 i 个元素的地址是:

LOC(aᵢ) = LOC(a₁) + (i − 1) × L

这个公式意味着随机存取:给下标就能算出地址,取任意元素都是 O(1)。这是顺序表独有的能力,链表做不到。

代价在插入和删除。在第 i 个位置插入,需要把第 i 到第 n 个元素整体后移一位,平均移动 n/2 次,时间复杂度 O(n);删除同理,平均移动 (n−1)/2 次,也是 O(n)。

按值查找仍要逐个比较,O(n)。按位查找 O(1)。

三、单链表:用指针换取灵活性

每个结点由数据域和指针域构成,指针指向下一个结点。物理位置不必相邻。

几个必考点:

  • 头指针 vs 头结点:头指针是必须有的,指向第一个结点;头结点是额外加在第一个元素之前的空结点,可以让"在表头插入/删除"和"在中间操作"的逻辑统一,不必特判。引入头结点不是必须的,但能少写很多 if。
  • 按位查找 O(n):链表不能算地址,只能从头顺着 next 走。
  • 插入删除 O(1)——前提是已经拿到了前驱结点。如果只知道位置 i,找前驱本身就要 O(n),所以"链表插入比顺序表快"这句话要带条件说。
  • 已知结点指针 p 要在其前插入:单链表做不到 O(1)(找不到前驱),常用技巧是"在 p 之后插入再交换数据",但这只是逻辑上等价,结点地址变了。

四、头插法与尾插法:一句代码决定顺序

用头插法建表,读入序列会被逆序存储;用尾插法(必须维护尾指针)则保持原序。这是上机题和选择题的高频细节。

带尾指针的循环单链表还有一个实用性质:尾指针的 next 就是头结点,因此在这种结构上"在表头插入"和"在表尾插入"都只要 O(1)。

五、双向链表:用空间换操作便利

每个结点多一个 prior 指针。好处是找前驱变成 O(1),插入删除的指针修改是对称的,但要注意顺序:在 p 之前插入 s 时,必须先让 s.prior 指向 p.prior、p.prior.next 指向 s,最后再处理 s.next 与 p.prior,顺序错了会断链。

双向循环链表是最常用的变体,判空条件是 head->next == head。

六、顺序表与链表的对比(这张表要背)

维度 顺序表 链表
存储方式 地址连续,逻辑相邻即物理相邻 地址任意,用指针连接
随机存取 支持,按位查找 O(1) 不支持,按位查找 O(n)
插入删除 需移动元素,平均 O(n) 已知前驱时 O(1)
空间 需预分配,可能浪费或溢出 动态分配,但每结点多花指针空间
存储密度 高(=1) 低(<1)
适用场景 表长稳定、频繁按位访问 频繁插入删除、表长变化大

选型口诀:查得多用顺序,改得多用链表;表长已知且稳定用顺序,长度不可预测用链表。

七、一元多项式加法

这是链表的经典应用:每个结点存系数和指数,两个多项式按指数递减有序。相加就是一次归并——比较当前结点指数,相等则系数相加(和为 0 就删掉该结点),不等则把指数大的接到结果链表上。时间复杂度 O(m+n)。

这个例子的价值在于:它演示了"用链表组织非数值数据 + 归并式处理",后面第 7 章的归并排序是同一个思路。

易错点清单

  1. 说"链表插入比顺序表快"时,必须补上前提:已定位到前驱。
  2. 头结点不是头指针;有头结点时,空表判据是 head->next == NULL,没有头结点时是 head == NULL。
  3. 顺序表插入的平均移动次数是 n/2,删除是 (n−1)/2,别记反。
  4. 双向链表插入四步指针的顺序不能乱。

这一章在整门课里的位置

第 2 章是后面所有"链式结构"的原型:第 3 章的链栈与链队列、第 5 章的二叉链表、第 6 章的邻接表,本质都是单链表加一个语义。这一章的指针操作不熟,后面每一章都会卡在同一个地方。反过来,本章的顺序表又是第 7 章"直接插入排序、希尔排序"的实现载体——排序讲的其实是"在顺序表上如何移动元素更少"。

补充:三段必须能默写的代码骨架

链表的题几乎都是这三段的变体,骨架记不住就永远写不对。

1. 头插法建表(结果与输入逆序)

s = (Node*)malloc(sizeof(Node));
s->data = x;
s->next = head->next;   // 先接后面
head->next = s;         // 再接前面

2. 尾插法建表(保持原序,必须维护尾指针 r)

s = (Node*)malloc(sizeof(Node));
s->data = x;
r->next = s;            // 挂到尾部
r = s;                  // 尾指针后移
r->next = NULL;         // 收尾置空,否则会出现野指针

3. 删除第 i 个结点(必须先找前驱)

p = head;  j = 0;
while (p && j < i-1) { p = p->next; j++; }   // 找第 i-1 个
if (!p || !p->next) return ERROR;            // 越界
q = p->next;  p->next = q->next;             // 摘链
free(q);                                     // 释放

三段的共同点是顺序不能换:先接后、再接前;先找前驱、再摘链、最后释放。顺序错了要么断链,要么内存泄漏。

本篇是《数据结构与算法(93 讲)》第 2 章的知识点摘要。完整板书与 C 代码见 课程页;上一章 绪论,下一章 栈与队列。

相关课程

这篇笔记涉及的主题,我录过对应的视频课程: