Skip to content

入门:节点、指针与链表的三种形态

基于通用数据结构概念 · 核于 2026-07

速查

  • 定义:链表是用指针把一组**分散在内存各处的节点(node)**串成一条链的线性结构,每个节点 = 数据域 + 指针域next 或再加 prev)。
  • 核心复杂度访问/按下标查 O(n)(从头节点逐个跳转);头部插入/删除 O(1)已知节点处插入/删除 O(1)(只改前后节点的指针);查找特定值 O(n)。
  • 三种形态单链表(只有 next,单向);双链表prev + next,双向,删节点无需前驱);循环链表(尾节点指回头节点,首尾相接)。
  • 与数组的核心差异:数组 O(1) 访问 / O(n) 增删;链表反过来 O(n) 访问 / O(1) 增删(已知节点)——选型看「访问多还是增删多」。
  • 缓存不友好:节点分散在堆各处,CPU 缓存行命中率极低,顺序遍历比数组慢一个数量级(即使大 O 相同)。
  • 虚拟头节点(dummy):在真实头节点前加一个不存数据的哨兵节点,让「对头节点的增删」与「对中间节点的增删」代码统一,消除空表/头节点特判。
  • 不能二分、不能随机访问:因 O(n) 访问,链表上一切需要「跳到第 i 个」的操作都要从头遍历。
  • 内存开销:每个节点多存 1~2 个指针;存 int 这类小元素时内存翻倍。
  • next 指针是「钥匙」:删节点时若先丢 next 就再也找不回后续节点——「先存 next 再改」是铁律。
  • 链表是手写结构的入门:栈/队列(两端操作)、哈希表(链地址法)、LRU 缓存(双向链表 + 哈希)都以它为载体。
  • 进阶顺序链表经典算法进阶操作参考

一、节点与指针:链表的物理模型

链表的基本单元是节点(node),每个节点存两样东西:

  • 数据域(val):实际承载的数据。
  • 指针域(next / prev):指向下一个(或上一个)节点的引用。
js
// 单链表节点
class ListNode {
  constructor(val, next = null) {
    this.val = val;    // 数据域
    this.next = next;  // 指针域:指向下一个节点
  }
}

// 构造 1 -> 2 -> 3 -> null
const head = new ListNode(1, new ListNode(2, new ListNode(3)));

与数组「一段连续内存 + 基地址」不同,链表的节点分散在堆内存各处,彼此靠 next 指针「跳转」连接。访问第 i 个元素必须从头节点 head 出发,顺着 next 走 i 步——这就是 O(n) 访问的根源。它派生三个推论:

  1. 不依赖连续内存:节点可以分配在任何空闲位置,申请大链表不会因碎片化失败。
  2. 增删只改指针:在「已知节点」处插入/删除,只需修改前后节点的 next,无需搬移任何元素,O(1) 完成。
  3. 缓存不友好:节点地址不连续,CPU 缓存行(通常 64 字节)无法预取后续节点,顺序遍历访存延迟高——这是链表遍历比数组慢一个数量级的主因。

二、核心复杂度

操作数组单链表双链表
按下标访问 a[i] / node(i)O(1)O(n)O(n)
头部插入/删除O(n)O(1)O(1)
尾部插入O(1)(动态摊还)O(1)(带尾指针)/ O(n)O(1)(带尾指针)
尾部删除O(1)O(n)(要找前驱)O(1)
中间增删(已知位置)O(n)O(1)(单链表删除需前驱)O(1)
查找(按值)O(n)O(n)O(n)
二分查找O(log n)O(n)(不能二分)O(n)

记住一句话:「访问多、要二分、要随机访问 → 数组;增删多、尤其头部增删 → 链表」。注意单链表「删除某节点」若只给了该节点指针、没给前驱,需 O(n) 找前驱才能改 next——双链表靠 prev 指针规避了这点。

三、三种链表形态

单链表(Singly Linked List)

每个节点只有一个 next 指针,只能从头单向走到尾,尾节点的 nextnull。这是面试题默认的「链表」形态,最常考。

js
class ListNode {
  constructor(val, next = null) {
    this.val = val;
    this.next = next;
  }
}

优点是结构简单、每节点只多存一个指针;缺点是只能单向遍历,删除某节点(只给该节点指针)需 O(n) 找前驱,不能回退。

双链表(Doubly Linked List)

每个节点有 prevnext 两个指针,可双向遍历。给定一个节点指针,删除它只需 node.prev.next = node.next; node.next.prev = node.prev,O(1) 完成——无需像单链表那样找前驱。

js
class DListNode {
  constructor(val, prev = null, next = null) {
    this.val = val;
    this.prev = prev;
    this.next = next;
  }
}

代价是每节点多存一个指针(内存翻倍)。Java LinkedList、C++ std::list、LRU 缓存底层都是双链表。

循环链表(Circular Linked List)

尾节点的 next 不指 null,而是指回头节点,整个链表首尾相接成环。可以是单循环链表,也可以是双循环链表。常用于「约瑟夫环」「轮询调度」「缓冲区」等需要循环处理的场景。

单循环:head -> A -> B -> C -> head(C.next = head)
双循环:head <-> A <-> B <-> C <-> head(首尾 prev/next 互指)

判断循环链表「遍历结束」不能用 curr.next === null,而要看 curr.next === head(或用 curr === head 起始 + 至少走一步)。

四、与数组对比

维度数组(动态)链表
随机访问 a[i] / node(i)O(1)O(n) ❌
头部增删O(n) ❌O(1)
尾部增删O(1)(摊还)✅O(1)(带尾指针)✅
中间增删(已知位置)O(n)O(1)
顺序遍历缓存友好(连续内存)✅差(节点分散)❌
内存开销仅数据每节点多 1~2 个指针
二分查找支持不支持 ❌
内存布局必须连续分散,按需分配

选型口诀:「读多写少 / 要随机访问 / 要二分 / 要排序 → 数组;写多读少 / 频繁头插 / 大小不确定且频繁中间插删 → 链表」。实际工程里,由于现代 CPU 缓存的强大,即使增删多也常常优先用动态数组(连续内存带来的缓存收益常压过指针操作的常数节省)——链表多用于 LRU 缓存、链式哈希桶、多项式、实现栈/队列等特定场景。

五、内存模型:节点分散

堆内存(分散):
+------+      +------+      +------+
| 1 | ●-|---->| 3 | ●-|---->| 5 | / |
+------+      +------+      +------+
  head          node2         node3
  (● 是 next 指针,/ 是 null)

每个节点单独 new 出来,地址不连续。好处是无需扩容(加节点就 new,不占大块连续内存);坏处是每节点独立寻址 + 指针跳转,缓存命中差。

为什么链表遍历比数组慢

虽然单链表顺序遍历也是 O(n),但实际跑起来比数组慢一个数量级,原因:

  • 缓存不命中:数组连续,CPU 预取连续缓存行;链表节点分散,每跳一个节点都可能 cache miss(一次访存 ~100 周期)。
  • 指针解引用:每次 curr = curr.next 都是一次间接寻址,破坏流水线。
  • 内存碎片:节点散落各处,TLB 命中也差。

所以「链表增删 O(1)」只在不计缓存、不计常数的理论分析下成立;工程上小数据量数组常更快。

六、虚拟头节点与哨兵技巧

虚拟头节点(dummy head)

在真实头节点之前加一个不存数据的哨兵节点 dummy,让 dummy.next = head。返回时取 dummy.next

js
// 在有序链表中插入一个新节点(用 dummy 统一边界)
function insert(head, node) {
  const dummy = new ListNode(0, head);
  let prev = dummy;
  while (prev.next && prev.next.val < node.val) prev = prev.next;
  node.next = prev.next;   // 先接后
  prev.next = node;        // 再接前
  return dummy.next;       // 返回新头(head 可能变)
}

好处:统一了「对头节点的操作」和「对中间节点的操作」——不用单独写「若插入到头节点之前则更新 head」的特判。涉及「可能改变头节点」的题(删除节点、合并、反转区段)几乎都该用 dummy。

哨兵节点(sentinel)

更广义地,「哨兵」是放在边界处简化判断的虚拟节点。如双向链表的头尾哨兵headtail 是两个空节点,真实节点夹在中间,首尾插入删除都变成「中间插入删除」,无需特判空表/单元素。

下一步

理解了链表的节点模型与三种形态后,下一步是链表最高频的算法套路——反转、环检测、找中点、倒数第 k、合并,它们几乎全是双指针的应用,见链表经典算法