链表(Linked List)

链表题看起来在操作节点,实际上大多数都在考三件事:找前驱、断开再重连、用双指针控制相对位置。

核心思路

链表是一种线性数据结构,通过指针将一组零散的内存块串联起来使用。每个内存块称为链表的”节点”(Node),除了存储数据外,还需记录链上下一个节点的地址。

核心特点

  1. 非连续存储:节点在内存中不连续
  2. 动态大小:可以灵活增删节点
  3. 指针连接:通过指针维护节点关系
  4. 无法随机访问:必须从头节点开始遍历

链表类型

1. 单链表(Singly Linked List)

┌────┬────┐    ┌────┬────┐    ┌────┬────┐
│ 1  │ ●──┼───→│ 2  │ ●──┼───→│ 3  │null│
└────┴────┘    └────┴────┘    └────┴────┘
  data next      data next      data next

2. 双向链表(Doubly Linked List)

     ┌────┬────┬────┐    ┌────┬────┬────┐    ┌────┬────┬────┐
null←┼─●  │ 1  │ ●──┼───→│ ●  │ 2  │ ●──┼───→│ ●  │ 3  │ ●──┼→null
     └────┴────┴────┘    └────┴────┴────┘    └────┴────┴────┘
     prev data next      prev data next      prev data next

3. 循环链表(Circular Linked List)

     ┌────┬────┐    ┌────┬────┐    ┌────┬────┐
  ┌─→│ 1  │ ●──┼───→│ 2  │ ●──┼───→│ 3  │ ●──┼──┐
  │  └────┴────┘    └────┴────┘    └────┴────┘  │
  └───────────────────────────────────────────────┘

基本操作

时间复杂度

操作单链表双向链表说明
访问O(n)O(n)需要从头遍历
搜索O(n)O(n)需要遍历
插入(头部)O(1)O(1)直接操作
插入(尾部)O(n)O(1)**需维护tail指针
插入(中间)O(1)O(1)已知前驱节点
删除(头部)O(1)O(1)直接操作
删除(中间)O(1)O(1)已知前驱节点

空间复杂度

  • 单链表:每个节点额外存储 1 个指针
  • 双向链表:每个节点额外存储 2 个指针

Go 代码

节点定义

type ListNode struct {
    Val  int
    Next *ListNode
}
 
// 双向链表节点
type DoublyListNode struct {
    Val  int
    Prev *DoublyListNode
    Next *DoublyListNode
}

反转链表

func reverseList(head *ListNode) *ListNode {
	var prev *ListNode
	cur := head
 
	for cur != nil {
		next := cur.Next
		cur.Next = prev
		prev = cur
		cur = next
	}
 
	return prev
}

虚拟头节点

func removeNthFromEnd(head *ListNode, n int) *ListNode {
	dummy := &ListNode{Next: head}
	fast, slow := dummy, dummy
 
	for i := 0; i < n; i++ {
		fast = fast.Next
	}
	for fast.Next != nil {
		fast = fast.Next
		slow = slow.Next
	}
 
	slow.Next = slow.Next.Next
	return dummy.Next
}

常用技巧

1. 虚拟头节点(Dummy Head)

简化边界条件处理

经典题目

基础操作

双指针技巧

链表重组

高级题目

2. 快慢指针

  • 找中点
  • 判断有环
  • 找环入口
  • 删除倒数第 n 个节点

链表 vs 数组

特性数组链表
内存分配连续离散
访问速度O(1)O(n)
插入删除O(n)O(1)*
缓存友好是否
空间利用高低(额外指针)
大小固定/扩容动态

*前提是已知插入/删除位置的前驱节点

易错点

链表题几乎所有 bug 都和指针顺序有关,尤其是“先断开还是先保存 next”。

  • 改指针前先把 next 存下来。
  • 涉及头节点删除时,优先上虚拟头节点。
  • 快慢指针先把循环条件写稳,再写指针移动。
  • 合并 / 反转 / 重排时,最后要检查尾节点是否正确断开。

优缺点

优点

  • ✅ 插入删除效率高(已知位置)
  • ✅ 动态大小,灵活扩展
  • ✅ 不需要连续内存

缺点

  • ❌ 无法随机访问
  • ❌ 额外存储指针,空间开销大
  • ❌ 缓存不友好,局部性差

应用场景

  1. 频繁插入删除:任务队列、LRU缓存
  2. 大小不确定:动态数据集合
  3. 实现其他数据结构:栈、队列、图的邻接表

相关主题

  • 栈 - 可用链表实现
  • 队列 - 可用链表实现
  • 哈希表 - 链地址法使用链表解决冲突
  • 跳表 - 基于链表的改进

返回:数据结构 | 算法学习导航