链表
- 什么是链表
- 内存中的存储方式
- 顺序访问
- 添加和删除数据
- 循环链表与双向链表
- 时间复杂度
什么是链表
链表也是一种数据呈线性排列的数据结构,但与数组不同:链表中的数据分散存储在内存的各个地方,每个元素通过一个「指针」指向下一个元素的地址,把它们串在一起。
你可以把链表想象成一场寻宝游戏:你到达第一个地点,找到一张纸条写着「下一个地点在 123 号」;到了 123 号,又找到一张纸条写着「下一个地点在 456 号」……就这样一环扣一环地找下去。
┌──────┬──┐ ┌──────┬──┐ ┌──────┬────┐
│ Blue │ ─┼──→ │Yellow│ ─┼──→ │ Red │null│
└──────┴──┘ └──────┴──┘ └──────┴────┘
数据 指针 数据 指针 数据 指针(不指向任何位置)
每个元素包含两部分:数据和指针。指针记录了下一个元素的内存地址。最后一个元素的指针为 null,表示链表到此结束。
内存中的存储方式
内存地址: 200 530 120
┌──────┬───┐ ┌──────┬───┐ ┌──────┬────┐
│ Blue │530│ │Yellow│120│ │ Red │null│
└──────┴───┘ └──────┴───┘ └──────┴────┘
↑ ↑ ↑
头节点 第二个 第三个(尾节点)
数据分散在内存各处,不需要连续的空间——这是链表和数组最大的区别。
顺序访问
因为数据分散存储,想访问第 3 个元素,不能像数组那样直接 a[2],必须从头开始,顺着指针一步步走过去:
从头开始 → Blue → 顺着指针 → Yellow → 顺着指针 → Red (找到了!)
这就是顺序访问(Sequential Access)。如果目标在链表末尾,需要走遍整个链表,所以访问速度比数组慢。
添加和删除数据
链表的添加和删除非常方便——只需要改几个指针的指向就行,不需要像数组那样移动大量元素。
添加数据(在 Blue 和 Yellow 之间插入 Green):
添加前: Blue ──→ Yellow ──→ Red
步骤1: 把 Blue 的指针指向 Green
Blue ──→ Green Yellow ──→ Red
步骤2: 把 Green 的指针指向 Yellow
Blue ──→ Green ──→ Yellow ──→ Red
完成!只改了两个指针
删除数据(删除 Yellow):
删除前: Blue ──→ Green ──→ Yellow ──→ Red
步骤: 把 Green 的指针从 Yellow 改为 Red
Blue ──→ Green ──→ Red Yellow (变成孤儿,无法访问)
完成!Yellow 还在内存中,但没人能访问它了
循环链表与双向链表
循环链表(环形链表):尾部的指针不指向 null,而是指向头节点,形成环形。
Blue ──→ Yellow ──→ Red ──┐
↑ │
└───────────────────────┘
没有头和尾的概念,适合保存数量固定的最新数据(如循环缓冲区)。
双向链表:每个节点有两个指针,分别指向前驱和后继。
┌──────┬──┬──┐ ┌──────┬──┬──┐ ┌──────┬──┬──┐
│ Blue │ │ ─┼──→ │Yellow│ ─│ ─┼──→ │ Red │ ─│ │
│ │ │←─┼─── │ │←─│ │←───│ │ │ │
└──────┴──┴──┘ └──────┴──┴──┘ └──────┴──┴──┘
前驱 后继 前驱 后继 前驱 后继
不仅可以从前往后走,还可以从后往前走。缺点是:每个节点多存一个指针,占用更多空间;增删时需要修改更多指针。
时间复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 访问(按索引) | O(n) | 必须从头开始遍历 |
| 查找(搜索) | O(n) | 需要逐个比较 |
| 在头部插入 | O(1) | 改两个指针即可 |
| 在尾部插入 | O(1) | 改两个指针即可(有尾指针时) |
| 在中间插入 | O(1) | 前提是已经到达插入位置 |
| 删除 | O(1) | 前提是已经到达删除位置 |
小结
- 链表的数据分散存储,通过指针串联,不需要连续内存
- 访问慢(O(n) 顺序访问),但增删快(O(1))
- 数组适合频繁读取,链表适合频繁增删
- 变体有循环链表(环形)和双向链表(可双向遍历)