Week 41 @ 2024 算法周记【双指针链表】
- 1 LC 21. Merge Two Sorted Lists 合并两个有序列表
- 2 LC 86. Partition List 分隔链表
- 3 LC 23. Merge k Sorted Lists 合并 k 个有序列表
- 4 LC 19. Remove Nth Node From End of List 删除链表的倒数第 N 个结点
- 5 LC 876. Middle of the Linked List 链表的中间结点(找到链表的中点)
- 6 LC 141. Linked List Cycle 环形链表(判断链表是否有环)
- 7 LC 160. Intersection of Two Linked Lists 相交链表(判断两个链表是否相交)
LC 21. Merge Two Sorted Lists 合并两个有序列表
https://leetcode.com/problems/merge-two-sorted-lists/
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
dummy := &ListNode{}
p := dummy
p1 := list1
p2 := list2
for p1 != nil && p2 != nil {
if p1.Val < p2.Val {
p.Next = p1
p1 = p1.Next
p = p.Next
} else {
p.Next = p2
p2 = p2.Next
p = p.Next
}
}
for p1 != nil {
p.Next = p1
p1 = p1.Next
p = p.Next
}
for p2 != nil {
p.Next = p2
p2 = p2.Next
p = p.Next
}
return dummy.Next
}
优化解法
最后去合并剩余的 list1 / list2 时,不需要逐节点合并,只需要让 p.Next 指向最终剩余的 list 即可(其他节点已经「组成好一个有序链表」了)
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
dummy := &ListNode{}
p := dummy
p1 := list1
p2 := list2
for p1 != nil && p2 != nil {
if p1.Val < p2.Val {
p.Next = p1
p1 = p1.Next
p = p.Next
} else {
p.Next = p2
p2 = p2.Next
p = p.Next
}
}
if p1 != nil {
p.Next = p1
}
if p2 != nil {
p.Next = p2
}
return dummy.Next
}
LC 86. Partition List 分隔链表
https://leetcode.com/problems/partition-list/
题意:看上去返回的是一个链表,实际上是返回了两个链表连起来的。。
相当于生成两个新的链表,一个链表的所有值都 < x,另一个都 >=x ,再将这两个链表合到一起
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func partition(head *ListNode, x int) *ListNode {
dummy1 := &ListNode{}
dummy2 := &ListNode{}
p1 := dummy1
p2 := dummy2
for p := head; p != nil; p = p.Next {
if p.Val < x {
p1.Next = p
p1 = p1.Next
} else {
p2.Next = p
p2 = p2.Next
}
}
p1.Next = dummy2.Next
p2.Next = nil
return dummy1.Next
}
LC 23. Merge k Sorted Lists 合并 k 个有序列表
https://leetcode.com/problems/merge-k-sorted-lists/
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeKLists(lists []*ListNode) *ListNode {
dummy := &ListNode{}
p := dummy
for {
hasAny := false
mi := -1
mv := 1000000
mp := (*ListNode)(nil)
for i, h := range lists {
if h == nil {
continue
}
hasAny = true
if h.Val < mv {
mi = i
mv = h.Val
mp = h
}
}
if !hasAny {
break
}
p.Next = mp
p = p.Next
lists[mi] = lists[mi].Next
}
return dummy.Next
}
上述代码时间复杂度为 O(lists.length * max(lists[i].length))
,即 O(NM)
考虑条件
0 <= lists[i].length <= 500
k == lists.length
,0 <= k <= 10^4
最大为 5 * 10^6 可以通过 OJ
优化方案
将所有优化方案
将所有 list 的头节点加入堆中,时间复杂度降为 O(sum(lists[i].length) * log(lists.length))
,即 O(NlogK)
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func mergeKLists(lists []*ListNode) *ListNode {
dummy := &ListNode{}
p := dummy
pq := &NodeHeap{}
for _, l := range lists {
if l != nil {
heap.Push(pq, l)
}
}
for pq.Len() != 0 {
v := heap.Pop(pq).(*ListNode)
p.Next = v
p = p.Next
if v.Next != nil {
heap.Push(pq, v.Next)
}
}
return dummy.Next
}
type NodeHeap []*ListNode
func (h NodeHeap) Len() int {
return len(h)
}
func (h NodeHeap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
func (h NodeHeap) Less(i, j int) bool {
return h[i].Val < h[j].Val
}
func (h *NodeHeap) Push(x any) {
*h = append(*h, x.(*ListNode))
}
func (h *NodeHeap) Pop() any {
n := len(*h)
v := (*h)[n-1]
*h = (*h)[:n-1]
return v
}
LC 19. Remove Nth Node From End of List 删除链表的倒数第 N 个结点
https://leetcode.com/problems/remove-nth-node-from-end-of-list/
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func removeNthFromEnd(head *ListNode, n int) *ListNode {
p1 := head
p2 := head
for i := 0; i < n; i++ {
p2 = p2.Next
}
prev := (*ListNode)(nil)
for p2 != nil {
p2 = p2.Next
prev = p1
p1 = p1.Next
}
if prev == nil {
return head.Next
}
prev.Next = prev.Next.Next
return head
}
本题重点在于利用双指针找到倒数第 x 个节点
将找到倒数第 x 个节点封装成函数 findFromEnd
,题目可以转变为 找到倒数第 n+1 个节点 然后进行操作
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{Next: head}
prev := findFromEnd(dummy, n+1)
prev.Next = prev.Next.Next
return dummy.Next
}
func findFromEnd(head *ListNode, n int) *ListNode {
p1 := head
p2 := head
for i := 0; i < n; i++ {
p2 = p2.Next
}
for p2 != nil {
p2 = p2.Next
p1 = p1.Next
}
return p1
}
LC 876. Middle of the Linked List 链表的中间结点(找到链表的中点)
https://leetcode.com/problems/middle-of-the-linked-list/
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func middleNode(head *ListNode) *ListNode {
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
return slow
}
快慢指针:奇数时 slow 指向中间节点、偶数时 slow 指向中间两节点中后面那个
进阶问题:如何让偶数时指向前面那个?
答:引入一个额外的 dummy 头节点,与原链组成新链,利用该算法对新链取中间节点即可
原链为奇数 [0, 2p+1) → 新链为偶数 [0, 2p+2) → 取中点获得新链的 p+1 号节点 → 等同原链的 p 号节点
原链为偶数 [0, 2p) → 新链为奇数 [0, 2p+1) → 取中点获得新链的 p 号节点 → 等同原链的 p-1 号节点
LC 2095. Delete the Middle Node of a Linked List 删除链表的中间节点
https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func deleteMiddle(head *ListNode) *ListNode {
slow := head
fast := head
prev := (*ListNode)(nil)
for fast != nil && fast.Next != nil {
prev = slow
slow = slow.Next
fast = fast.Next.Next
}
if prev == nil { // only one node
return nil
}
prev.Next = prev.Next.Next
return head
}
在 Week 41 @ 2024 算法周记【双指针链表】 | Middle of the Linked List 的基础上,记录 slow 前面那个指针
LC 141. Linked List Cycle 环形链表(判断链表是否有环)
https://leetcode.com/problems/linked-list-cycle/
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func hasCycle(head *ListNode) bool {
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
return true
}
}
return false
}
LC 142. Linked List Cycle II 环形链表 II (找到环形链表的起点)
https://leetcode.com/problems/linked-list-cycle-ii/
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func detectCycle(head *ListNode) *ListNode {
slow := head
fast := 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 // no cycle
}
slow = head
for slow != fast {
slow = slow.Next
fast = fast.Next
}
return slow
}
解法说明:快慢指针相遇时,让其中一个指针指回起点,再重新同速行进再次遇到则为环起点
LC 160. Intersection of Two Linked Lists 相交链表(判断两个链表是否相交)
https://leetcode.com/problems/intersection-of-two-linked-lists/description/
/**
* Definition for singly-linked list.
* type ListNode struct {
* Val int
* Next *ListNode
* }
*/
func getIntersectionNode(headA, headB *ListNode) *ListNode {
p1 := headA
p2 := headB
for p1 != p2 {
if p1 == nil {
p1 = headB
} else {
p1 = p1.Next
}
if p2 == nil {
p2 = headA
} else {
p2 = p2.Next
}
}
return p1
}
👆解法为交替行走,即 p1 走完 A 走 B、p2 走完 B 走 A,目的是让数值上二者走了相同的路径,最终相交于一点
设 A 长 a+m、B 长 b+m,则 p1 走 a+m+b、p2 走 b+m+a 时相交
无相交点即 m=0 的情况,无需特殊处理
解法 2:与上述解法思路相同,但不那么强求一个 for 循环,分别计算两个链表长度找到差值后前后指针即可
解法 3:简单粗暴哈希表
解法 4:将 tail.Next 与 headB 相连,即成一个「链+圈」结构,用 Week 41 @ 2024 算法周记【双指针链表】 | Linked List Cycle II 环形链表 II (找到环形链表的起点) 算法即可解决