理论分析
考察的题目基本和链表的特点是一致的,当然这里的特点也是和数组相比较而言的,正如一开始学习数据结构的时候,总是要比较链表和数组一样,只不过很多题目表明将简单的原理实际应用起来,还是很费劲的,就和物理数学公式很少,但是题目很难一样。
链表的特点第一个就是查询起来费劲,需要O(n),就是要从头开始一直找下去;第二个特点就是构造起来也没那么爽快但又比较灵活,由于查起来费劲,你得记录一个位置,后面从这个位置继续处理。灵活本身就是比较好,但是会导致考的题目比较多样,数组构造很固定,链表可以头插,可以尾插。第三个特点就是处理起来费劲,必须要处理A,你不能直接处理了A就结束了,还要考虑A后面的东西,A前面的东西,这里就需要一是技巧(虚拟头)二是细节梳理(找一个例子先比划清楚)。
总结:
- 查找O(n),需要有一些查询技巧。
- 构造灵活,需要记录插入点,需要会两种构造方式。
- 处理本节点费劲,对应就需要注意细节。
基本套路
针对上面的两个点,分别对应一些套路
查找
- 中间节点:快慢指针,一个走1步,一个每次走两步,直到快的走到尾部。
- 倒数第K个节点:快慢指针,一个先走K个,然后一起走,直到快的走到尾部。
构造
- 头插:记录当前的头结点,新来的节点为新的头,会导致链表逆序/反转
- 尾插:记录当前的尾节点,新来的节点为新的尾,要注意封尾(新来的节点可能尾部不干净),
处理
- 删除(前任删除法):必须要用前一个节点进行处理,这时候会有很多特例,用虚拟头统一化
- 修改Next(临时备胎):用一个备胎记录当前节点,这样再修改就无后顾之忧了
- 处理某个区间:for循环内找到第一个要处理的区域之后,开启for循环小区间一次性处理(不然太费劲了)
- 使用Next.Next要慎重
常见示例
反转链表(206)
题目描述,详见:https://leetcode.cn/problems/UHnkqh/description/
解法1:
func reverseList(head *ListNode) *ListNode {
var newHead *ListNode
// 遍历一遍旧的,把旧的节点,挨个头插到新节点上
for head != nil {
curNode := head
head = head.Next // 当前节点处理的细节,因为要操作当前节点同时旧链表还要往后走,所以这里采用临时变量法
curNode.Next = newHead
newHead = curNode
}
return newHead
}
用到的技巧分别是
- 头插实现逆序
- 修改单个节点:临时备胎记录
解法2:
很多人会说,使用递归的方式实现,然后给出的例子是简化后的,让人学起来一头雾水。
实际上循环和递归有时候本身就是一样的,这里给出照着循环写的递归逻辑:
func reverseList(head *ListNode) *ListNode {
return reverse(nil, head)
}
func reverse(newHead, head *ListNode) *ListNode {
if head == nil { // for 循环的退出条件
return newHead
}
// 下面这部分和解法1一样
curNode := head
head = head.Next
curNode.Next = newHead
newHead = curNode
// 进入下一个“for”循环
return reverse(newHead, head)
}
链表的中间节点(876)
题目描述,详见:https://leetcode.cn/problems/middle-of-the-linked-list/description/
很明显是查找-中间节点的题目,但是上面只给了基本的思路,细节需要手动推演一遍(19题有具体推演示例)。
在纸上根据例子推演一下一节,即可通过。
func middleNode(head *ListNode) *ListNode {
fast := head
slow := head
for fast != nil && fast.Next != nil {
fast = fast.Next.Next // 细节1 fast.Next如果是nil 这里就不行了,所以for循环增加判断
slow = slow.Next
}
// 之后根据细节1 以及题目的两个例子,可是slow即为答案,无需额外处理
return slow
}
删除链表的中间节点(2095)
题目:https://leetcode.cn/problems/delete-the-middle-node-of-a-linked-list/description/?envType=study-plan-v2&envId=leetcode-75
思考:中间节点–快慢指针;删除节点–前任删除法(虚拟头),找到前一个注意细节。
这个题目相对于 链表的中间节点 这个要找的目标变了变成他的前一个了,正好删除要用虚拟头,正好不就是对于加了虚拟的的链表,找到这个新链表的中间节点。
:::info
注意对于极端例子的手动模拟,根据模拟到的细节,进行编码
:::
部分正确的解法
func deleteMiddle(head *ListNode) *ListNode {
dummyHead := &ListNode{Next:head}
// 现在的目标:找到dummyHead链表的中间节点,甚至可以完全复用另一个题目的函数
fast := dummyHead
slow := dummyHead
for fast != nil && fast.Next != nil {
fast = fast.Next.Next
slow = slow.Next
}
// return slow,这里slow就是前任啊
// 删除slow的Next 手动演一遍,注意细节
slow.Next = slow.Next.Next
return dummyHead.Next
}
按这个套路来,因为比较自信,没有对所有Case进行推演(19题有具体推演示例),梳理细节,导致总长度偶数个的链表可以过,总长度奇数个的过不了。
这个思路肯定是没问题的,因为没别的方法了,那就需要调整判断条件,经过推演,需要增加这个判断条件fast.Next.Next != nil,才能在总长度为奇数个的时候,让中间节点指向真正的前任(这里有点绕,详见下文总结)。
func deleteMiddle(head *ListNode) *ListNode {
dummyHead := &ListNode{Next:head}
// 现在的目标:找到dummyHead链表的中间节点,甚至可以完全复用另一个题目的函数
fast := dummyHead
slow := dummyHead
for fast != nil && fast.Next != nil && fast.Next.Next != nil {
fast = fast.Next.Next
slow = slow.Next
}
// return slow,这里slow就是前任啊
// 删除slow的Next 手动演一遍,注意细节
slow.Next = slow.Next.Next
return dummyHead.Next
}
总结:
本题和上一个题目,基本都是找到中间节点,只不过针对链表长度是奇数的时候,意见一致,针对链表总长度是偶数的时候有分歧。一个是找前半部分,一个是找后半部分,这里通过修改判断条件来分别处理。偶数个的举例如下:
假设 1->2->3->4这4个点,找后半部分,则中间节点是3号,找前半部分则中间节点是2号。
删除链表的倒数第N个节点(19)
题目:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/description/
快慢指针,先走K步,定位的细节手动推演即可。删除:前任删除法,虚拟头.
推演举例:
head = [1,2,3,4,5], n = 2
增加了个虚拟头,变成了 虚拟头,1,2,3,4,5,当前目的是slow走到3就不走了。
- fast 先走两步到了2上,slow还在虚拟头上
- 一步一步走,此时slow到了3上,fast到了5上,必须不能继续了。这时候for循环的判断条件就出来了,fast.Next != nil
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummyHead := &ListNode{Next:head}
slow := dummyHead
fast := dummyHead
for n > 0 {
fast = fast.Next
n--
}
for fast.Next != nil {
fast = fast.Next
slow = slow.Next
}
slow.Next = slow.Next.Next
return dummyHead.Next
}
合并两个有序链表(21)
题目:21. 合并两个有序链表
- 打算把2放到1上,这时候1的第一个节点可能不是最小的,那就给1加个虚拟头
- 尾插法,记录尾部,不断往尾部添加,注意封尾!
给出的代码可以进一步精简,但是这里表示了具体的思考过程。
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
dummyHead := &ListNode{Next:list1}
p := dummyHead
// 下面的for循环的条件,需要根据例子进行手动推演
for p.Next != nil && list2 != nil {
if p.Next.Val <= list2.Val {
p = p.Next
} else {
cur := list2
list2 = list2.Next
cur.Next = p.Next
p.Next = cur
p = p.Next
}
}
// 注意本题推演之后的特例比较多,这里的特例实际上就是上面退出循环的时候对应的情况
// 1. list2用完了,list1就是剩下的啥都不用干 2. list1 用完了 那list2还得继续处理
if list2 != nil {
p.Next = list2
}
return dummyHead.Next
}
指定区间反转链表(92)
题目:92. 反转链表 II
:::info
注意各种变形题目,非常常考
:::
func reverseBetween(head *ListNode, left int, right int) *ListNode {
// 按index找到某个位置,之后一顿操作,就用小循环法,这样简单清晰
// 手动演一遍,之后编码即可,或者一边写代码一边手动推演
index := 0
dummyHead := &ListNode{Next:head} // 用虚拟头 是因为left的边界条件
p := dummyHead
for p.Next != nil {
index++ // 这个和 index := 0 是联动的,咋弄都行,现在的目标就是找到left
if index >= left {
// 找到了的情况,这里先写一个壳子 即if index >= left { break },然后把没找到的情况 head = head.Next 写完
// 开启小循环处理,手动推演 具体翻转流程
var newHead *ListNode// 翻转,肯定用之前的头插法,就需要newHead
// 这里之前常用的套路是head,没弄过Next,所以还是用head
head = p.Next
for index <= right { // 这里具体要不要等于,手动推演确定
// Index 也得进步,具体位置推演确定
cur := head
head = head.Next
index++ // 放这里,和head的调整放一起
cur.Next = newHead
newHead = cur
}
// 翻转之后要放回来 根据推演,这时候p在1上,p.Next是2,head是5
p.Next.Next = head
p.Next = newHead
break // 因为是连续的,干完就跑路
}
p = p.Next
}
return dummyHead.Next
}
删除的新思路(237)
题目:237. 删除链表中的节点
对本节点的val进行赋值为下一个节点的值,之后删除下一个节点。
总结
本文给出了基本理论和解题思路,以及给出了部分实例和思考过程,具体思路和题目如下:
查找:
- 中间节点:快慢指针,一个走1步,一个每次走两步,直到快的走到尾部。(876、2095)
- 倒数第K个节点:快慢指针,一个先走K个,然后一起走,直到快的走到尾部。(19)
构造:
- 头插:记录当前的头结点,新来的节点为新的头,会导致链表逆序/反转(206)
- 尾插:记录当前的尾节点,新来的节点为新的尾,要注意封尾(新来的节点可能尾部不干净)。(21)
处理:
- 删除(前任删除法):必须要用前一个节点进行处理,这时候会有很多特例,用虚拟头统一化。(2095、19)
- 修改Next(临时备胎):用一个备胎记录当前节点,这样再修改就无后顾之忧了。(206)
- 处理某个区间:for循环内找要处理的区域开始点,开启for循环小区间一次性处理(不然太费劲了)。(92)