单链表(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 中构建链式结构的标准手法。当 Next 为 nil 时,表示当前节点是链表的最后一个节点。
链表本身需要维护头指针和长度信息,方便快速判断空表和获取大小:
// 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 (循环结束)
关键在于每次循环做三件事:
- 暂存
cur.Next到next - 将
cur.Next翻转为prev prev和cur各前进一步
六、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)
单链表是所有链式数据结构的基石。理解了单链表的节点结构、指针操作和遍历模式,后续学习双链表、循环链表、栈、队列等结构都会水到渠成。关键记住一句话:链表的所有操作本质上都是指针的重新连接。

浙公网安备 33010602011771号