AIGC标识 单链表(Singly Linked List)

单链表(Singly Linked List)

一、什么是单链表

单链表是一种线性数据结构,它通过指针将一系列分散在内存中的节点串联起来。每个节点包含两部分:数据域(存储实际值)和指针域(指向下一个节点的内存地址)。与数组不同,单链表的节点在物理内存中不需要连续存放,这使得它在插入和删除时具有天然优势。

数组 vs 单链表对比

维度 数组 单链表
内存布局 连续分配 分散分配,指针串联
随机访问 O(1),直接下标 O(n),必须从头遍历
头部插入 O(n),需要搬迁元素 O(1),修改指针即可
尾部插入 均摊 O(1) O(1)(维护尾指针)/ O(n)(无尾指针)
中间插入/删除 O(n),元素搬迁 O(1)(已知位置时只需改指针)
内存开销 仅存储数据 额外存储一个指针
缓存友好性 高(连续内存命中缓存行) 低(指针跳转导致缓存失效)

核心结论:单链表适合"频繁插入删除、很少随机访问"的场景,如实现哈希桶、LRU 缓存等。

二、节点结构设计

单链表的最小单元是节点。在 Go 中,用结构体定义节点非常自然:

// Node 单链表节点
type Node struct {
    Data int   // 数据域
    Next *Node // 指针域,指向下一个节点
}

Next 字段的类型是 *Node——指向自身类型的指针,这是 Go 中构建链式结构的标准手法。当 Nextnil 时,表示当前节点是链表的最后一个节点。

链表本身需要维护头指针和长度信息,方便快速判断空表和获取大小:

// LinkedList 单链表
type LinkedList struct {
    head *Node // 头指针
    len  int   // 链表长度
}

三、核心操作与时间复杂度

3.1 头部插入 — O(1)

头部插入是单链表最高效的操作。新节点的 Next 指向当前头节点,然后将头指针更新为新节点即可:

插入前: head -> [A] -> [B] -> nil
插入 X: head -> [X] -> [A] -> [B] -> nil

只需两步赋值,与链表长度无关。

3.2 尾部插入 — O(1) 或 O(n)

  • 维护尾指针:如果链表结构中额外保存 tail 指针,尾部插入也是 O(1)
  • 无尾指针:需要从头遍历到最后一个节点,时间复杂度 O(n)

3.3 按位置插入 — O(n)

在指定索引处插入节点,需要先遍历到该位置的前驱节点,然后调整指针。遍历过程是 O(n),指针调整本身是 O(1)。

3.4 删除节点 — O(n)

删除操作本身只需修改前驱节点的 Next 指针(O(1)),但找到前驱节点需要遍历(O(n))。特殊地,删除头节点是 O(1)。

3.5 查找 — O(n)

单链表不支持随机访问,查找特定值或特定位置的节点必须从头开始逐一遍历。

3.6 遍历 — O(n)

从 head 开始,沿 Next 指针依次访问每个节点直到 nil。

四、完整实现代码

package main

import "fmt"

// Node 单链表节点
type Node struct {
    Data int
    Next *Node
}

// LinkedList 单链表
type LinkedList struct {
    head *Node
    len  int
}

// NewLinkedList 创建空链表
func NewLinkedList() *LinkedList {
    return &LinkedList{}
}

// InsertHead 头部插入 O(1)
func (l *LinkedList) InsertHead(val int) {
    l.head = &Node{Data: val, Next: l.head}
    l.len++
}

// InsertTail 尾部插入 O(n)
func (l *LinkedList) InsertTail(val int) {
    newNode := &Node{Data: val}
    if l.head == nil {
        l.head = newNode
        l.len++
        return
    }
    cur := l.head
    for cur.Next != nil {
        cur = cur.Next
    }
    cur.Next = newNode
    l.len++
}

// InsertAt 指定位置插入 O(n)
// index 为 0 表示头部插入,index 为 len 表示尾部插入
func (l *LinkedList) InsertAt(index, val int) bool {
    if index < 0 || index > l.len {
        return false
    }
    if index == 0 {
        l.InsertHead(val)
        return true
    }
    cur := l.head
    for i := 0; i < index-1; i++ {
        cur = cur.Next
    }
    cur.Next = &Node{Data: val, Next: cur.Next}
    l.len++
    return true
}

// DeleteHead 删除头节点 O(1)
func (l *LinkedList) DeleteHead() (int, bool) {
    if l.head == nil {
        return 0, false
    }
    val := l.head.Data
    l.head = l.head.Next
    l.len--
    return val, true
}

// DeleteVal 删除第一个匹配的节点 O(n)
func (l *LinkedList) DeleteVal(val int) bool {
    if l.head == nil {
        return false
    }
    // 头节点就是目标
    if l.head.Data == val {
        l.head = l.head.Next
        l.len--
        return true
    }
    cur := l.head
    for cur.Next != nil {
        if cur.Next.Data == val {
            cur.Next = cur.Next.Next
            l.len--
            return true
        }
        cur = cur.Next
    }
    return false
}

// Find 查找节点 O(n)
func (l *LinkedList) Find(val int) *Node {
    cur := l.head
    for cur != nil {
        if cur.Data == val {
            return cur
        }
        cur = cur.Next
    }
    return nil
}

// Reverse 反转链表 O(n)
// 经典的三指针迭代法
func (l *LinkedList) Reverse() {
    var prev *Node
    cur := l.head
    for cur != nil {
        next := cur.Next // 暂存后继
        cur.Next = prev  // 翻转指向
        prev = cur       // prev 前进
        cur = next       // cur 前进
    }
    l.head = prev
}

// Traverse 遍历输出
func (l *LinkedList) Traverse() []int {
    result := make([]int, 0, l.len)
    cur := l.head
    for cur != nil {
        result = append(result, cur.Data)
        cur = cur.Next
    }
    return result
}

// Len 返回链表长度
func (l *LinkedList) Len() int {
    return l.len
}

func main() {
    list := NewLinkedList()

    // 尾部插入 1,2,3
    list.InsertTail(1)
    list.InsertTail(2)
    list.InsertTail(3)
    fmt.Println("尾部插入 1,2,3:", list.Traverse()) // [1 2 3]

    // 头部插入 0
    list.InsertHead(0)
    fmt.Println("头部插入 0:", list.Traverse()) // [0 1 2 3]

    // 位置 2 插入 99
    list.InsertAt(2, 99)
    fmt.Println("位置2插入99:", list.Traverse()) // [0 1 99 2 3]

    // 删除值 99
    list.DeleteVal(99)
    fmt.Println("删除值99:", list.Traverse()) // [0 1 2 3]

    // 反转
    list.Reverse()
    fmt.Println("反转后:", list.Traverse()) // [3 2 1 0]

    // 查找
    node := list.Find(2)
    if node != nil {
        fmt.Println("查找值2:", node.Data) // 2
    }

    fmt.Println("链表长度:", list.Len()) // 4
}

运行结果

尾部插入 1,2,3: [1 2 3]
头部插入 0: [0 1 2 3]
位置2插入99: [0 1 99 2 3]
删除值99: [0 1 2 3]
反转后: [3 2 1 0]
查找值2: 2
链表长度: 4

五、链表反转详解

链表反转是面试高频题,核心思路是三指针迭代法

初始状态: nil <- prev    [A] -> [B] -> [C] -> nil
                          cur    next

第一步:   nil <- [A] <- prev    [B] -> [C] -> nil
                         cur    next

第二步:   nil <- [A] <- [B] <- prev    [C] -> nil
                                cur    next

最终:     nil <- [A] <- [B] <- [C] <- prev
                                       cur=nil (循环结束)

关键在于每次循环做三件事:

  1. 暂存 cur.Nextnext
  2. cur.Next 翻转为 prev
  3. prevcur 各前进一步

六、Go 语言链表的内存管理

Go 的垃圾回收机制让链表操作比 C 语言简单得多。在 C 中删除节点后必须手动 free(),否则内存泄漏。而在 Go 中,只要某个节点不再被任何指针引用,GC 会自动回收它。

删除节点时只需将前驱的 Next 指向被删节点的后继,被删节点自然变成不可达对象,等待 GC 回收:

// 删除 cur.Next 指向的节点
cur.Next = cur.Next.Next
// 被删除的节点不再被引用,GC 会自动回收

七、常见面试题与技巧

7.1 快慢指针检测环

快指针每次走两步,慢指针每次走一步。如果存在环,快指针一定会追上慢指针。

7.2 快慢指针找中点

慢指针到尾时,快指针刚好到中点。常用于归并排序的链表版本。

7.3 哨兵节点简化边界处理

在头节点前添加一个虚拟哨兵节点(dummy head),可以统一处理头部和非头部的插入/删除逻辑,避免大量 if l.head == nil 的特殊判断。

八、总结

操作 时间复杂度 空间复杂度
头部插入 O(1) O(1)
尾部插入 O(n) / O(1)* O(1)
指定位置插入 O(n) O(1)
删除头节点 O(1) O(1)
删除指定值 O(n) O(1)
查找 O(n) O(1)
反转 O(n) O(1)
遍历 O(n) O(1)

*维护尾指针时尾部插入为 O(1)

单链表是所有链式数据结构的基石。理解了单链表的节点结构、指针操作和遍历模式,后续学习双链表、循环链表、栈、队列等结构都会水到渠成。关键记住一句话:链表的所有操作本质上都是指针的重新连接

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