链表题目之基本技巧方法篇

理论分析

考察的题目基本和链表的特点是一致的,当然这里的特点也是和数组相比较而言的,正如一开始学习数据结构的时候,总是要比较链表和数组一样,只不过很多题目表明将简单的原理实际应用起来,还是很费劲的,就和物理数学公式很少,但是题目很难一样。
链表的特点第一个就是查询起来费劲,需要O(n),就是要从头开始一直找下去;第二个特点就是构造起来也没那么爽快但又比较灵活,由于查起来费劲,你得记录一个位置,后面从这个位置继续处理。灵活本身就是比较好,但是会导致考的题目比较多样,数组构造很固定,链表可以头插,可以尾插。第三个特点就是处理起来费劲,必须要处理A,你不能直接处理了A就结束了,还要考虑A后面的东西,A前面的东西,这里就需要一是技巧(虚拟头)二是细节梳理(找一个例子先比划清楚)。

总结:

  1. 查找O(n),需要有一些查询技巧。
  2. 构造灵活,需要记录插入点,需要会两种构造方式。
  3. 处理本节点费劲,对应就需要注意细节。

基本套路

针对上面的两个点,分别对应一些套路

查找

  1. 中间节点:快慢指针,一个走1步,一个每次走两步,直到快的走到尾部。
  2. 倒数第K个节点:快慢指针,一个先走K个,然后一起走,直到快的走到尾部。

构造

  1. 头插:记录当前的头结点,新来的节点为新的头,会导致链表逆序/反转
  2. 尾插:记录当前的尾节点,新来的节点为新的尾,要注意封尾(新来的节点可能尾部不干净),

处理

  1. 删除(前任删除法):必须要用前一个节点进行处理,这时候会有很多特例,用虚拟头统一化
  2. 修改Next(临时备胎):用一个备胎记录当前节点,这样再修改就无后顾之忧了
  3. 处理某个区间:for循环内找到第一个要处理的区域之后,开启for循环小区间一次性处理(不然太费劲了)
  4. 使用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
}

用到的技巧分别是

  1. 头插实现逆序
  2. 修改单个节点:临时备胎记录

解法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就不走了。

  1. fast 先走两步到了2上,slow还在虚拟头上
  2. 一步一步走,此时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. 合并两个有序链表

  1. 打算把2放到1上,这时候1的第一个节点可能不是最小的,那就给1加个虚拟头
  2. 尾插法,记录尾部,不断往尾部添加,注意封尾!

给出的代码可以进一步精简,但是这里表示了具体的思考过程。

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. 中间节点:快慢指针,一个走1步,一个每次走两步,直到快的走到尾部。(876、2095)
  2. 倒数第K个节点:快慢指针,一个先走K个,然后一起走,直到快的走到尾部。(19)

构造:

  1. 头插:记录当前的头结点,新来的节点为新的头,会导致链表逆序/反转(206)
  2. 尾插:记录当前的尾节点,新来的节点为新的尾,要注意封尾(新来的节点可能尾部不干净)。(21)

处理:

  1. 删除(前任删除法):必须要用前一个节点进行处理,这时候会有很多特例,用虚拟头统一化。(2095、19)
  2. 修改Next(临时备胎):用一个备胎记录当前节点,这样再修改就无后顾之忧了。(206)
  3. 处理某个区间:for循环内找要处理的区域开始点,开启for循环小区间一次性处理(不然太费劲了)。(92)
  • 23
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值