这张图是自己总结的,目前觉得链表相关基础的有章可循的知识还是覆盖到了的。
反转链表
这个真是值得好好说道说道,四个题目层层递进,很有代表性。
1. 基础模版
这个不会=没刷过leetcode
理论:这个图很多人都画过,反转链表就是这个原理,但是这张图是哥们儿自己画的。
代码:
func reverseList(head *ListNode) *ListNode {
var dummy *ListNode
pre, cur := dummy, head
for cur != nil {
nxt := cur.Next
cur.Next = pre
pre = cur
cur = nxt
}
return pre
}
2. 反转指定范围的链表
稍微进阶版本其实就是,给出指定下标left,right,要求反转[left,right]的节点,其他节点顺序不变。
理论:这个题无论题解画的多清楚,永远不如自己画一遍图,于是我就画了一遍图。
代码:
func reverseBetween(head *ListNode, left int, right int) *ListNode {
dummy := &ListNode{Next:head}
p0 := dummy
for i := 0; i < left - 1; i++ {
p0 = p0.Next
}
var pre, cur *ListNode = nil, p0.Next // pre 为nil, 而不是p0
for i := 0; i <= right - left; i++ {
nxt := cur.Next
cur.Next = pre
pre = cur
cur = nxt
}
p0.Next.Next = cur
p0.Next = pre
return dummy.Next // 必须返回dummy.Next 而不是 head
}
3. k个一组反转链表
你可能会疑惑为什么我跳过了第三个题目(两两交换链表中的节点),因为我认为第三个题目就是k个一组反转链表中,k=2的情况。
而k个一组反转链表,只需要在「反转链表2」的基础上,知道:
- 一共有几组要反转,即 lenght / k,lenght 为链表的总长度。
- 每组要反转的次数,即k。
值得一提的是「哨兵节点p0」的更新。
直接看代码:
func reverseKGroup(head *ListNode, k int) *ListNode {
p := head
length := 0
for p != nil {
p = p.Next
length++
}
dummy := &ListNode{Next:head}
p0 := dummy
var pre, cur *ListNode = nil, p0.Next
for i := 0; i < length/k; i++ {
for j := 0; j < k; j++ {
nxt := cur.Next
cur.Next = pre
pre = cur
cur = nxt
}
// tmp 是刚刚被反转过的这一小坨的尾巴
tmp := p0.Next
p0.Next.Next = cur
p0.Next = pre
p0 = tmp
}
return dummy.Next
}
前后指针
1. 删除链表倒数第k个节点
前后指针可以干啥?
答:可以找倒数第N个节点。
画图:
代码:
func removeNthFromEnd(head *ListNode, n int) *ListNode {
// 1. 创建dummy
d := &ListNode{Next: head}
l, r := d, d
// 2. 拉开距离
for i := 0; i < n; i++ {
r = r.Next
}
// 3. 一起走
for r.Next != nil {
r = r.Next
l = l.Next
}
// 4. 找到节点,干事儿
l.Next = l.Next.Next
return d.Next
}
快慢指针
我理解快慢指针是个工具,这个工具可以干2个事儿。
快慢指针相关的题目:
我来说几个有代表性的:
1. 删除中间节点
跟找中点稍有不同,删除链表的中间节点,需要找到中间节点的前一个节点,记录pre即可。
func deleteMiddle(head *ListNode) *ListNode {
dummy := &ListNode{Next:head}
pre, f, s := dummy, head, head
for f != nil && f.Next != nil {
f = f.Next.Next
s = s.Next
pre = pre.Next
}
pre.Next = pre.Next.Next
return dummy.Next
}
说句题外话,其实我刚开始写的比这个还sao一点儿,不需要pre,s直接指向dummy效果也是一样的。但是,看了灵神的代码之后,还是觉得这样清晰一些。
2. 回文链表
思路是这样的:
代码:(这代码是直接复制灵神的,我自己第一次写也过了,但是思路太烂,多了一步计算链表长度)
// 876. 链表的中间结点
func middleNode(head *ListNode) *ListNode {
slow, fast := head, head
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
return slow
}
// 206. 反转链表
func reverseList(head *ListNode) *ListNode {
var pre, cur *ListNode = nil, head
for cur != nil {
nxt := cur.Next
cur.Next = pre
pre = cur
cur = nxt
}
return pre
}
func isPalindrome(head *ListNode) bool {
mid := middleNode(head)
head2 := reverseList(mid)
for head2 != nil {
if head.Val != head2.Val { // 不是回文链表
return false
}
head = head.Next
head2 = head2.Next
}
return true
}
3. 重排链表
重拍链表题目描述:
思路:
代码:
func reorderList(head *ListNode) {
mid := getMid(head)
l, r := head, reverse(mid.Next)
mid.Next = nil
for r != nil {
rNext := r.Next
lNext := l.Next
r.Next = l.Next
l.Next = r
r = rNext
l = lNext
}
}
func getMid(h *ListNode) *ListNode {
s, f := h, h
for f != nil && f.Next != nil {
f = f.Next.Next
s = s.Next
}
return s
}
func reverse(h *ListNode) *ListNode {
var dummy *ListNode
pre, cur := dummy, h
for cur != nil {
nxt := cur.Next
cur.Next = pre
pre = cur
cur = nxt
}
return pre
}
4. 环形链表2
找到环形链表入环的那个节点并返回。
思路:自己25年1月份写的,用了leetcode官方题解的图,我觉得还是很清晰的。
快慢指针走,快指针为nil,证明无环,快慢指针相遇,快指针回到链表头,一起以1的速度前进,相遇点即为目标点。
func detectCycle(head *ListNode) *ListNode {
s, f := head, head
for {
if f == nil || f.Next == nil {
return nil
}
f = f.Next.Next
s = s.Next
if f == s {
f = head
break
}
}
for {
if f == s {
break
}
f = f.Next
s = s.Next
}
return s
}
合并链表
合并链表只是一种题目类型,其实并没有什么对应的算法,把过程模拟好即可。其实本质上非常像归并排序中的merge。
题目:
1. 合并2个有序链表
双指针遍历。
代码:
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
dummy := &ListNode{}
p := dummy
for list1 != nil && list2 != nil {
if list1.Val < list2.Val {
p.Next = list1
list1 = list1.Next
} else {
p.Next = list2
list2 = list2.Next
}
p = p.Next
}
for list1 != nil {
p.Next = list1
list1 = list1.Next
p = p.Next
}
for list2 != nil {
p.Next = list2
list2 = list2.Next
p = p.Next
}
return dummy.Next
}
2. 合并k个有序链表
思路:
- 搓一个merge。
- 分治:用分解问题的思路,两个整数l,r,表示当前要处理的区间。
递归树:(自己画的,有不合理的地方请指正)
代码:
func mergeKLists(lists []*ListNode) *ListNode {
if len(lists) == 0 {
return nil
}
return proc(lists, 0, len(lists)-1)
}
// 闭区间[l, r]
func proc(lists []*ListNode, l, r int) *ListNode {
if l == r {
return lists[l]
}
mid := (l + r) / 2
left, right := proc(lists, l, mid), proc(lists, mid+1, r)
return merge(left, right)
}
func merge(a, b *ListNode) *ListNode {
dummy := new(ListNode)
p := dummy
for a != nil && b != nil {
if a.Val < b.Val {
p.Next = a
a = a.Next
} else {
p.Next = b
b = b.Next
}
p = p.Next
}
for a != nil {
p.Next = a
a = a.Next
p = p.Next
}
for b != nil {
p.Next = b
b = b.Next
p = p.Next
}
return dummy.Next
}
双指针
其实快慢指针,前后指针,合并链表等类型,都用到了双指针的思想。只不过,有些题目就是单纯的双指针,比如这个相交链表。
1. 相交链表
题目:
思路:
两个指针p,q 分别指向head1,head2,p走到头,就指向head2(最初指向head1),q走到头,就指向head1(最初指向head2)。p,q继续往前走,如果相遇,相遇点就是交点(为什么看下图一目了然),如果不相遇(一方再次为nil),说明2条链表不相交。
代码:
func getIntersectionNode(headA, headB *ListNode) *ListNode {
a, b := headA, headB
flagA, flagB := true, true
for {
// 首次为nil,换头
if a == nil && flagA {
a = headB
flagA = false
}
if b == nil && flagB {
b = headA
flagB = false
}
// 二次为nil,不相交
if a == nil || b == nil {
return nil
}
// 相遇,找到交点
if a == b {
break
}
a = a.Next
b = b.Next
}
return a
}
删点
1. 删除链表中的重复元素2
2和1的区别是,2是所有重复元素都删除,1是重复元素只保留一个。
思路:对每一个值,都记录前一个节点pre,如果值重复,则删除,否则继续。说白了就是模拟。
代码:
func deleteDuplicates(head *ListNode) *ListNode {
dummy := &ListNode{Next: head}
pre, cur := dummy, head
for cur != nil {
// 一定会往前走1格
p := cur
for p != nil && p.Val == cur.Val {
p = p.Next
}
if cur.Next == p {
// 更新pre
pre = cur
} else {
// 删除所有重复值
pre.Next = p
}
cur = p
}
return dummy.Next
}
排序链表
用分治算法,我比较容易理解。
- [head, tail)代表我要排序的区间,如果区间长度为1或0,则直接返回,否则merge两个更小级别的子问题。
- 上来先手撕一个标准merge,然后写proc递归函数。
func sortList(head *ListNode) *ListNode {
return proc(head, nil)
}
func proc(head, tail *ListNode) *ListNode {
if head == nil {
return head
}
if head.Next == tail {
head.Next = nil
return head
}
f, s := head, head
for f != tail && f.Next != tail {
f = f.Next.Next
s = s.Next
}
left, right := proc(head, s), proc(s, tail)
return merge(left, right)
}
func merge(a, b *ListNode) *ListNode {
dummy := &ListNode{}
p := dummy
for a != nil && b != nil {
if a.Val < b.Val {
p.Next = a
a = a.Next
} else {
p.Next = b
b = b.Next
}
p = p.Next
}
for a != nil {
p.Next = a
a = a.Next
p = p.Next
}
for b != nil {
p.Next = b
b = b.Next
p = p.Next
}
return dummy.Next
}