AIGC标识 双链表(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 缓存的标配数据结构。

六、双链表的指针维护要点

双链表的每个插入/删除操作需要同时维护两个方向的指针,容易出错。编写时遵循以下检查清单:

  1. 插入时:新节点的 Prev 和 Next 都要设置;前驱的 Next 和后继的 Prev 都要更新
  2. 删除时:前驱的 Next 和后继的 Prev 都要更新;如果删除的是头/尾节点,head/tail 指针也要更新
  3. 边界处理:空表、单节点表、头/尾位置是最容易出 bug 的地方
  4. nil 检查:操作 Prev 前检查是否为头节点(Prev == nil),操作 Next 前检查是否为尾节点(Next == nil)

七、循环双链表

在双链表基础上,如果让 head.Prev = tailtail.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 缓存、任务调度队列)中,双链表是不可替代的选择。

posted @ 2026-07-27 09:03  FfHUCisI  阅读(16)  评论(0)    收藏  举报