链表详解 本文详细介绍链表(Linked List)这种数据结构,包括单链表、双向链表的基本操作,以及链表相关的经典算法问题。
什么是链表? 链表是一种线性数据结构,通过指针将一系列节点连接起来。与数组不同,链表中的元素在内存中不是连续存储的。
链表的特点
动态大小 :可以根据需要动态分配内存
非连续存储 :节点在内存中不一定连续
插入/删除高效 :在已知位置插入或删除的时间复杂度为 O(1)
随机访问低效 :访问第 k 个元素需要 O(k) 时间
链表的分类 flowchart TD
Root["链表"]
Single["单链表 每个节点只有一个指针"]
Double["双向链表 每个节点有两个指针"]
Circular["循环链表 尾节点指向头节点"]
Root --> Single
Root --> Double
Root --> Circular
style Root fill:#e1f5ff
style Single fill:#e8f5e9
style Double fill:#fff3e0
style Circular fill:#f3e5f5
单链表 单链表(Singly Linked List)是最简单的链表形式,每个节点包含数据和指向下一个节点的指针。
节点定义 1 2 3 4 5 type ListNode struct { Val int Next *ListNode }
基本操作 1. 创建链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func NewListNode (val int ) *ListNode { return &ListNode{Val: val, Next: nil } } func CreateList (vals []int ) *ListNode { if len (vals) == 0 { return nil } head := NewListNode(vals[0 ]) curr := head for i := 1 ; i < len (vals); i++ { curr.Next = NewListNode(vals[i]) curr = curr.Next } return head }
2. 遍历链表 1 2 3 4 5 6 7 8 9 10 func Traverse (head *ListNode) []int { var result []int curr := head for curr != nil { result = append (result, curr.Val) curr = curr.Next } return result }
3. 查找节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func FindNode (head *ListNode, val int ) *ListNode { curr := head for curr != nil { if curr.Val == val { return curr } curr = curr.Next } return nil } func GetNodeAt (head *ListNode, k int ) *ListNode { curr := head for i := 0 ; i < k && curr != nil ; i++ { curr = curr.Next } return curr }
4. 插入节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func InsertAfter (node *ListNode, val int ) { if node == nil { return } newNode := NewListNode(val) newNode.Next = node.Next node.Next = newNode } func InsertAtHead (head **ListNode, val int ) { newNode := NewListNode(val) newNode.Next = *head *head = newNode }
5. 删除节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 func DeleteNode (head **ListNode, val int ) bool { if *head == nil { return false } if (*head).Val == val { *head = (*head).Next return true } curr := *head for curr.Next != nil { if curr.Next.Val == val { curr.Next = curr.Next.Next return true } curr = curr.Next } return false } func DeleteNodeAt (head **ListNode, k int ) bool { if k == 0 { if *head != nil { *head = (*head).Next return true } return false } curr := *head for i := 0 ; i < k-1 && curr != nil ; i++ { curr = curr.Next } if curr == nil || curr.Next == nil { return false } curr.Next = curr.Next.Next return true }
复杂度分析
操作
时间复杂度
空间复杂度
访问第k个元素
O(k)
O(1)
在头部插入
O(1)
O(1)
在尾部插入
O(n)
O(1)
在指定位置插入
O(k)
O(1)
删除节点
O(n)
O(1)
查找
O(n)
O(1)
双向链表 双向链表(Doubly Linked List)的每个节点包含两个指针,分别指向前一个节点和后一个节点。
节点定义 1 2 3 4 5 6 type DoublyListNode struct { Val int Prev *DoublyListNode Next *DoublyListNode }
基本操作 1. 创建双向链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func NewDoublyListNode (val int ) *DoublyListNode { return &DoublyListNode{Val: val, Prev: nil , Next: nil } } func CreateDoublyList (vals []int ) *DoublyListNode { if len (vals) == 0 { return nil } head := NewDoublyListNode(vals[0 ]) curr := head for i := 1 ; i < len (vals); i++ { newNode := NewDoublyListNode(vals[i]) curr.Next = newNode newNode.Prev = curr curr = newNode } return head }
2. 插入节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func InsertAfter (node *DoublyListNode, val int ) { if node == nil { return } newNode := NewDoublyListNode(val) newNode.Next = node.Next newNode.Prev = node if node.Next != nil { node.Next.Prev = newNode } node.Next = newNode } func InsertBefore (node *DoublyListNode, val int ) { if node == nil { return } newNode := NewDoublyListNode(val) newNode.Prev = node.Prev newNode.Next = node if node.Prev != nil { node.Prev.Next = newNode } node.Prev = newNode }
3. 删除节点 1 2 3 4 5 6 7 8 9 10 11 12 13 func DeleteDoublyNode (node *DoublyListNode) { if node == nil { return } if node.Prev != nil { node.Prev.Next = node.Next } if node.Next != nil { node.Next.Prev = node.Prev } }
双向链表的优势
双向遍历 :可以从前往后或从后往前遍历
删除高效 :在已知节点的情况下,删除操作是 O(1)
插入灵活 :可以在节点前后插入
链表是否交叉 问题描述 判断两个单链表是否相交,如果相交,找出相交的起始节点。
算法原理 方法 1:哈希表 使用哈希表存储第一个链表的所有节点,然后遍历第二个链表,检查是否有节点在哈希表中。
方法 2:双指针(推荐)
计算两个链表的长度
让较长的链表先走 |lenA - lenB| 步
两个指针同时移动,如果相遇则相交
方法 3:连接链表 将两个链表首尾相连,如果相交,会形成环,问题转化为判断链表是否有环。
算法流程 flowchart TD
Start["开始"] --> LenA["计算链表A的长度"]
LenA --> LenB["计算链表B的长度"]
LenB --> Diff["计算长度差 diff"]
Diff --> Long["让较长链表的指针先走diff步"]
Long --> Move["两个指针同时移动"]
Move --> Check{指针相遇?}
Check -->|是| Intersect["链表相交"]
Check -->|否| NoIntersect["链表不相交"]
Intersect --> End["返回相交节点"]
NoIntersect --> End
style Start fill:#e1f5ff
style End fill:#e8f5e9
style Check fill:#fff3e0
实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 func GetIntersectionNode (headA, headB *ListNode) *ListNode { if headA == nil || headB == nil { return nil } lenA := getLength(headA) lenB := getLength(headB) currA, currB := headA, headB if lenA > lenB { for i := 0 ; i < lenA-lenB; i++ { currA = currA.Next } } else { for i := 0 ; i < lenB-lenA; i++ { currB = currB.Next } } for currA != nil && currB != nil { if currA == currB { return currA } currA = currA.Next currB = currB.Next } return nil } func getLength (head *ListNode) int { length := 0 curr := head for curr != nil { length++ curr = curr.Next } return length } func GetIntersectionNodeHash (headA, headB *ListNode) *ListNode { visited := make (map [*ListNode]bool ) curr := headA for curr != nil { visited[curr] = true curr = curr.Next } curr = headB for curr != nil { if visited[curr] { return curr } curr = curr.Next } return nil }
时间复杂度 : O(m + n),其中 m 和 n 是两个链表的长度空间复杂度 : O(1)(双指针)或 O(m)(哈希表)
链表是否带环 问题描述 判断一个单链表是否有环(循环)。
算法原理:快慢指针(Floyd判圈算法) 使用两个指针,一个慢指针每次移动一步,一个快指针每次移动两步。如果链表有环,快慢指针最终会相遇。
算法流程 flowchart TD
Start["开始"] --> Init["初始化: slow = head, fast = head"]
Init --> Loop{fast != nil && fast.Next != nil?}
Loop -->|否| NoCycle["无环"]
Loop -->|是| Move["slow = slow.Next fast = fast.Next.Next"]
Move --> Check{slow == fast?}
Check -->|是| HasCycle["有环"]
Check -->|否| Loop
HasCycle --> End["返回true"]
NoCycle --> End2["返回false"]
style Start fill:#e1f5ff
style End fill:#e8f5e9
style End2 fill:#e8f5e9
style Check fill:#fff3e0
实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func HasCycle (head *ListNode) bool { if head == nil || head.Next == nil { return false } slow, fast := head, head for fast != nil && fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { return true } } return false }
时间复杂度 : O(n)空间复杂度 : O(1)
环入口节点 问题描述 如果链表有环,找出环的入口节点。
算法原理
使用快慢指针找到相遇点
将一个指针重置到链表头部
两个指针同时移动,每次移动一步,相遇点即为环入口
数学证明 :
设链表头到环入口的距离为 a
环入口到相遇点的距离为 b
相遇点到环入口的距离为 c
快指针走过的距离:a + b + n(b + c)
慢指针走过的距离:a + b
因为快指针速度是慢指针的2倍:2(a + b) = a + b + n(b + c)
化简得:a = (n-1)(b+c) + c
这意味着从链表头到环入口的距离等于从相遇点到环入口的距离
算法流程 flowchart TD
Start["开始"] --> Find["使用快慢指针找到相遇点"]
Find --> Check{有环?}
Check -->|否| NoCycle["返回nil"]
Check -->|是| Reset["将slow重置到head"]
Reset --> Move["slow和fast同时移动,每次一步"]
Move --> Check2{slow == fast?}
Check2 -->|是| Entry["找到环入口"]
Check2 -->|否| Move
Entry --> End["返回入口节点"]
NoCycle --> End
style Start fill:#e1f5ff
style End fill:#e8f5e9
style Check fill:#fff3e0
style Check2 fill:#fff3e0
实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func DetectCycle (head *ListNode) *ListNode { if head == nil || head.Next == nil { return nil } slow, fast := head, head for fast != nil && fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { break } } if fast == nil || fast.Next == nil { return nil } slow = head for slow != fast { slow = slow.Next fast = fast.Next } return slow }
时间复杂度 : O(n)空间复杂度 : O(1)
反转链表 问题描述 反转一个单链表。
算法原理 方法 1:迭代法 使用三个指针:prev、curr、next,逐个反转节点。
方法 2:递归法 递归地反转后面的链表,然后将当前节点连接到反转后的链表。
算法流程(迭代法) flowchart TD
Start["开始"] --> Init["初始化: prev = nil, curr = head"]
Init --> Loop{curr != nil?}
Loop -->|否| End["返回prev"]
Loop -->|是| Save["next = curr.Next"]
Save --> Reverse["curr.Next = prev"]
Reverse --> Update["prev = curr curr = next"]
Update --> Loop
style Start fill:#e1f5ff
style End fill:#e8f5e9
style Reverse fill:#fff3e0
实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 func ReverseList (head *ListNode) *ListNode { var prev *ListNode curr := head for curr != nil { next := curr.Next curr.Next = prev prev = curr curr = next } return prev } func ReverseListRecursive (head *ListNode) *ListNode { if head == nil || head.Next == nil { return head } newHead := ReverseListRecursive(head.Next) head.Next.Next = head head.Next = nil return newHead } func ReverseListBetween (head *ListNode, left int , right int ) *ListNode { if head == nil || left == right { return head } dummy := &ListNode{Next: head} prev := dummy for i := 0 ; i < left-1 ; i++ { prev = prev.Next } curr := prev.Next for i := 0 ; i < right-left; i++ { next := curr.Next curr.Next = next.Next next.Next = prev.Next prev.Next = next } return dummy.Next }
时间复杂度 : O(n)空间复杂度 : O(1)(迭代)或 O(n)(递归)
判断是否是回文链表 问题描述 判断一个单链表是否为回文链表。
算法原理 方法 1:转换为数组 将链表转换为数组,然后判断数组是否为回文。
方法 2:快慢指针 + 反转(推荐)
使用快慢指针找到链表中点
反转后半部分链表
比较前半部分和反转后的后半部分
算法流程 flowchart TD
Start["开始"] --> Find["使用快慢指针找到中点"]
Find --> Reverse["反转后半部分链表"]
Reverse --> Compare["比较前半部分和后半部分"]
Compare --> Check{所有节点都相等?}
Check -->|是| Palindrome["是回文链表"]
Check -->|否| NotPalindrome["不是回文链表"]
Palindrome --> Restore["恢复原链表(可选)"]
NotPalindrome --> Restore
Restore --> End["返回结果"]
style Start fill:#e1f5ff
style End fill:#e8f5e9
style Check fill:#fff3e0
实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 func IsPalindrome (head *ListNode) bool { var vals []int curr := head for curr != nil { vals = append (vals, curr.Val) curr = curr.Next } left, right := 0 , len (vals)-1 for left < right { if vals[left] != vals[right] { return false } left++ right-- } return true } func IsPalindromeOptimized (head *ListNode) bool { if head == nil || head.Next == nil { return true } slow, fast := head, head for fast.Next != nil && fast.Next.Next != nil { slow = slow.Next fast = fast.Next.Next } secondHalf := ReverseList(slow.Next) p1, p2 := head, secondHalf isPalindrome := true for p2 != nil { if p1.Val != p2.Val { isPalindrome = false break } p1 = p1.Next p2 = p2.Next } slow.Next = ReverseList(secondHalf) return isPalindrome }
时间复杂度 : O(n)空间复杂度 : O(n)(方法1)或 O(1)(方法2)
合并有序链表 问题描述 将两个有序链表合并为一个新的有序链表。
算法原理 使用双指针,比较两个链表的节点值,将较小的节点连接到结果链表。
算法流程 flowchart TD
Start["开始"] --> Init["创建虚拟头节点dummy"]
Init --> Loop{两个链表都不为空?}
Loop -->|否| Append["将剩余链表连接到结果"]
Loop -->|是| Compare{list1.Val <= list2.Val?}
Compare -->|是| Add1["将list1的节点加入结果"]
Compare -->|否| Add2["将list2的节点加入结果"]
Add1 --> Move1["list1 = list1.Next"]
Add2 --> Move2["list2 = list2.Next"]
Move1 --> Loop
Move2 --> Loop
Append --> End["返回dummy.Next"]
style Start fill:#e1f5ff
style End fill:#e8f5e9
style Compare fill:#fff3e0
实现代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 func MergeTwoLists (list1 *ListNode, list2 *ListNode) *ListNode { dummy := &ListNode{} curr := dummy for list1 != nil && list2 != nil { if list1.Val <= list2.Val { curr.Next = list1 list1 = list1.Next } else { curr.Next = list2 list2 = list2.Next } curr = curr.Next } if list1 != nil { curr.Next = list1 } else { curr.Next = list2 } return dummy.Next } func MergeTwoListsRecursive (list1 *ListNode, list2 *ListNode) *ListNode { if list1 == nil { return list2 } if list2 == nil { return list1 } if list1.Val <= list2.Val { list1.Next = MergeTwoListsRecursive(list1.Next, list2) return list1 } else { list2.Next = MergeTwoListsRecursive(list1, list2.Next) return list2 } } func MergeKLists (lists []*ListNode) *ListNode { if len (lists) == 0 { return nil } return mergeKListsHelper(lists, 0 , len (lists)-1 ) } func mergeKListsHelper (lists []*ListNode, left, right int ) *ListNode { if left == right { return lists[left] } if left > right { return nil } mid := (left + right) / 2 list1 := mergeKListsHelper(lists, left, mid) list2 := mergeKListsHelper(lists, mid+1 , right) return MergeTwoLists(list1, list2) }
时间复杂度 : O(m + n),其中 m 和 n 是两个链表的长度空间复杂度 : O(1)(迭代)或 O(m + n)(递归)
其他经典问题 1. 删除链表的倒数第N个节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func RemoveNthFromEnd (head *ListNode, n int ) *ListNode { dummy := &ListNode{Next: head} first, second := dummy, dummy for i := 0 ; i <= n; i++ { first = first.Next } for first != nil { first = first.Next second = second.Next } second.Next = second.Next.Next return dummy.Next }
2. 两数相加 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func AddTwoNumbers (l1 *ListNode, l2 *ListNode) *ListNode { dummy := &ListNode{} curr := dummy carry := 0 for l1 != nil || l2 != nil || carry != 0 { sum := carry if l1 != nil { sum += l1.Val l1 = l1.Next } if l2 != nil { sum += l2.Val l2 = l2.Next } carry = sum / 10 curr.Next = &ListNode{Val: sum % 10 } curr = curr.Next } return dummy.Next }
3. 旋转链表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 func RotateRight (head *ListNode, k int ) *ListNode { if head == nil || head.Next == nil || k == 0 { return head } length := 1 tail := head for tail.Next != nil { length++ tail = tail.Next } k = k % length if k == 0 { return head } newTail := head for i := 0 ; i < length-k-1 ; i++ { newTail = newTail.Next } newHead := newTail.Next newTail.Next = nil tail.Next = head return newHead }
4. 交换相邻节点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func SwapPairs (head *ListNode) *ListNode { dummy := &ListNode{Next: head} prev := dummy for prev.Next != nil && prev.Next.Next != nil { first := prev.Next second := prev.Next.Next prev.Next = second first.Next = second.Next second.Next = first prev = first } return dummy.Next }
复杂度总结
操作
时间复杂度
空间复杂度
访问第k个元素
O(k)
O(1)
在头部插入
O(1)
O(1)
在尾部插入
O(n)
O(1)
删除节点
O(n)
O(1)
反转链表
O(n)
O(1)
判断回文
O(n)
O(1)
合并链表
O(m+n)
O(1)
判断是否有环
O(n)
O(1)
链表 vs 数组
特性
链表
数组
内存分配
动态
静态/动态
内存布局
非连续
连续
随机访问
O(n)
O(1)
插入/删除
O(1)(已知位置)
O(n)
缓存友好性
差
好
空间开销
每个节点需要额外指针
只需存储数据
应用场景
实现栈和队列 :使用链表可以高效地实现栈和队列
内存管理 :操作系统中的内存分配使用链表结构
图算法 :图的邻接表表示使用链表
LRU缓存 :使用双向链表实现LRU缓存
多项式运算 :使用链表存储多项式的系数和指数
总结
链表的特点 :动态大小、非连续存储、插入/删除高效
基本操作 :创建、遍历、插入、删除
经典问题 :
反转链表
判断回文
检测环
合并链表
删除倒数第N个节点
技巧 :
使用虚拟头节点简化操作
快慢指针解决环相关问题
双指针解决相交问题
掌握链表的基本操作和经典算法对于解决很多问题都非常有帮助!