双链表(Doubly Linked List)
双链表(Doubly Linked List)
一、为什么需要双链表
单链表有一个明显的短板:只能单向遍历。想要从当前节点回退到前驱节点,必须从头重新遍历。这个限制在某些场景下代价很大:
- LRU 缓存:需要快速将某个节点移到链表头部,必须知道前驱才能摘除它
- 浏览器前进/后退:需要双向导航
- 文本编辑器撤销/重做:双向链表天然适配
双链表在单链表节点的基础上增加了一个 prev 指针,使得每个节点同时持有前驱和后继的引用,实现了双向遍历能力。
单链表 vs 双链表
| 维度 | 单链表 | 双链表 |
|---|---|---|
| 节点结构 | Data + Next | Data + Next + Prev |
| 反向遍历 | 不支持 | 支持 |
| 删除已知节点 | 需找前驱 O(n) | 直接操作 O(1) |
| 内存开销 | 每节点 1 个指针 | 每节点 2 个指针 |
| 实现复杂度 | 较低 | 较高(需同时维护双向指针) |
二、节点结构设计
// DNode 双链表节点
type DNode struct {
Data int
Prev *DNode // 前驱指针
Next *DNode // 后继指针
}
双链表结构本身与单链表类似,维护头尾指针和长度:
// DoublyLinkedList 双链表
type DoublyLinkedList struct {
head *DNode
tail *DNode // 双链表通常维护尾指针,方便尾部 O(1) 操作
len int
}
双链表维护尾指针的成本更低——因为删除尾节点时可以直接通过 tail.Prev 找到新的尾节点,不需要遍历。这是双链表相比单链表的重要优势。
三、核心操作图解
3.1 尾部插入 — O(1)
插入前: [A] <-> [B] <-> nil
tail
插入 C:
1. newNode.Prev = tail (C 的前驱指向 B)
2. tail.Next = newNode (B 的后继指向 C)
3. tail = newNode (尾指针更新为 C)
结果: [A] <-> [B] <-> [C] <-> nil
tail
3.2 头部插入 — O(1)
插入前: nil <-> [A] <-> [B]
head
插入 X:
1. newNode.Next = head (X 的后继指向 A)
2. head.Prev = newNode (A 的前驱指向 X)
3. head = newNode (头指针更新为 X)
结果: nil <-> [X] <-> [A] <-> [B]
head
3.3 删除尾节点 — O(1)
这是双链表最闪光的操作。单链表删除尾节点需要 O(n) 遍历找倒数第二个节点,双链表通过 tail.Prev 直接获取:
删除前: [A] <-> [B] <-> [C]
tail
删除 C:
1. tail = tail.Prev (尾指针退回 B)
2. tail.Next = nil (B 的后继置空)
结果: [A] <-> [B]
tail
3.4 删除头节点 — O(1)
删除前: [A] <-> [B] <-> [C]
head
删除 A:
1. head = head.Next (头指针前进到 B)
2. head.Prev = nil (B 的前驱置空)
结果: [B] <-> [C]
head
四、完整实现代码
package main
import "fmt"
// DNode 双链表节点
type DNode struct {
Data int
Prev *DNode
Next *DNode
}
// DoublyLinkedList 双链表
type DoublyLinkedList struct {
head *DNode
tail *DNode
len int
}
func NewDoublyLinkedList() *DoublyLinkedList {
return &DoublyLinkedList{}
}
// InsertHead 头部插入 O(1)
func (dl *DoublyLinkedList) InsertHead(val int) {
newNode := &DNode{Data: val}
if dl.len == 0 {
dl.head = newNode
dl.tail = newNode
} else {
newNode.Next = dl.head
dl.head.Prev = newNode
dl.head = newNode
}
dl.len++
}
// InsertTail 尾部插入 O(1)
func (dl *DoublyLinkedList) InsertTail(val int) {
newNode := &DNode{Data: val}
if dl.len == 0 {
dl.head = newNode
dl.tail = newNode
} else {
newNode.Prev = dl.tail
dl.tail.Next = newNode
dl.tail = newNode
}
dl.len++
}
// DeleteHead 删除头节点 O(1)
func (dl *DoublyLinkedList) DeleteHead() (int, bool) {
if dl.len == 0 {
return 0, false
}
val := dl.head.Data
if dl.len == 1 {
dl.head = nil
dl.tail = nil
} else {
dl.head = dl.head.Next
dl.head.Prev = nil
}
dl.len--
return val, true
}
// DeleteTail 删除尾节点 O(1)
// 双链表的核心优势:无需遍历即可删除尾节点
func (dl *DoublyLinkedList) DeleteTail() (int, bool) {
if dl.len == 0 {
return 0, false
}
val := dl.tail.Data
if dl.len == 1 {
dl.head = nil
dl.tail = nil
} else {
dl.tail = dl.tail.Prev
dl.tail.Next = nil
}
dl.len--
return val, true
}
// ForwardTraverse 正向遍历 head -> tail
func (dl *DoublyLinkedList) ForwardTraverse() []int {
result := make([]int, 0, dl.len)
cur := dl.head
for cur != nil {
result = append(result, cur.Data)
cur = cur.Next
}
return result
}
// BackwardTraverse 反向遍历 tail -> head
// 双链表独有能力
func (dl *DoublyLinkedList) BackwardTraverse() []int {
result := make([]int, 0, dl.len)
cur := dl.tail
for cur != nil {
result = append(result, cur.Data)
cur = cur.Prev
}
return result
}
// Find 查找节点 O(n)
func (dl *DoublyLinkedList) Find(val int) *DNode {
cur := dl.head
for cur != nil {
if cur.Data == val {
return cur
}
cur = cur.Next
}
return nil
}
// DeleteNode 删除已知节点 O(1)
// 双链表的重要优势:已知节点引用时可直接删除,无需遍历找前驱
func (dl *DoublyLinkedList) DeleteNode(node *DNode) {
if node == nil {
return
}
if node.Prev != nil {
node.Prev.Next = node.Next
} else {
// 删除的是头节点
dl.head = node.Next
}
if node.Next != nil {
node.Next.Prev = node.Prev
} else {
// 删除的是尾节点
dl.tail = node.Prev
}
dl.len--
}
func main() {
dl := NewDoublyLinkedList()
// 尾部插入 10,20,30
dl.InsertTail(10)
dl.InsertTail(20)
dl.InsertTail(30)
fmt.Println("正向遍历:", dl.ForwardTraverse()) // [10 20 30]
fmt.Println("反向遍历:", dl.BackwardTraverse()) // [30 20 10]
// 头部插入 5
dl.InsertHead(5)
fmt.Println("头部插入5:", dl.ForwardTraverse()) // [5 10 20 30]
// 删除尾节点
val, _ := dl.DeleteTail()
fmt.Printf("删除尾节点(%d): %v\n", val, dl.ForwardTraverse()) // [5 10 20]
// 删除头节点
val, _ = dl.DeleteHead()
fmt.Printf("删除头节点(%d): %v\n", val, dl.ForwardTraverse()) // [10 20]
// 查找并删除已知节点
node := dl.Find(10)
dl.DeleteNode(node)
fmt.Println("删除值10后:", dl.ForwardTraverse()) // [20]
}
运行结果
正向遍历: [10 20 30]
反向遍历: [30 20 10]
头部插入5: [5 10 20 30]
删除尾节点(30): [5 10 20]
删除头节点(5): [10 20]
删除值10后: [20]
五、双链表删除已知节点的优势
这是双链表最重要的实战价值。考虑 LRU 缓存的场景:当缓存命中某个 key 时,需要将该节点移到链表头部。
单链表方案:必须从头遍历找到前驱节点 → O(n)
双链表方案:节点自身持有 Prev 指针,直接通过 node.Prev.Next = node.Next 完成摘除 → O(1)
// 双链表 O(1) 删除已知节点
func deleteKnownNode(node *DNode) {
node.Prev.Next = node.Next
node.Next.Prev = node.Prev
// GC 自动回收被摘除的节点
}
正是这个 O(1) 的删除能力,使得双链表成为实现 LRU 缓存的标配数据结构。
六、双链表的指针维护要点
双链表的每个插入/删除操作需要同时维护两个方向的指针,容易出错。编写时遵循以下检查清单:
- 插入时:新节点的 Prev 和 Next 都要设置;前驱的 Next 和后继的 Prev 都要更新
- 删除时:前驱的 Next 和后继的 Prev 都要更新;如果删除的是头/尾节点,head/tail 指针也要更新
- 边界处理:空表、单节点表、头/尾位置是最容易出 bug 的地方
- nil 检查:操作 Prev 前检查是否为头节点(Prev == nil),操作 Next 前检查是否为尾节点(Next == nil)
七、循环双链表
在双链表基础上,如果让 head.Prev = tail 且 tail.Next = head,就构成了循环双链表。这种结构下,从任意节点出发都可以遍历整个链表,且头尾判断更加灵活。Go 标准库 container/list 实现的就是循环双链表。
八、总结
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| 头部插入 | O(1) | 修改 2 个指针 |
| 尾部插入 | O(1) | 维护 tail 指针时 |
| 删除头节点 | O(1) | 修改 1 个指针 |
| 删除尾节点 | O(1) | 通过 tail.Prev,单链表需 O(n) |
| 删除已知节点 | O(1) | 双链表核心优势 |
| 查找 | O(n) | 最坏需遍历整个链表 |
| 正向遍历 | O(n) | head → tail |
| 反向遍历 | O(n) | tail → head,双链表独有 |
双链表用多一个指针的内存代价,换取了双向遍历和O(1) 删除已知节点两个关键能力。在需要频繁删除中间节点的场景(如 LRU/LFU 缓存、任务调度队列)中,双链表是不可替代的选择。

浙公网安备 33010602011771号