《剑指offer》16&19&20&22、链表四则:删除节点、倒数第k个节点、环的入口、有序链表合并

前言

《剑指offer》的16&19-22连续出现了多个链表类的题目,比之前的难度要稍微高一些。这时候一定要画图做分析,达到事半功倍的效果。
关于链表的结构请参考:
《剑指offer》预备知识-链表与二叉树
我会直接调用里面定义的这个链表的类
之前还出现了两道典型题目:05的链表反向输出和21的链表反转,可以参考这个笔记:
《剑指offer》05&21、链表反向输出与反转

关于链表其实网上有海量的文章,但是到了这份上相信对链表本身大家已经相当熟悉,只是一些思路不能很快的理清,也没有很clean的代码。我在文后摘录了一些其它代码供大家一起学习提高。我自己也对这些代码有一定的参考。

删除链表节点

分析

offer16的要求是,给出一个节点,要求从一个链表中删除该节点。
先考虑简单的情形:
1、整个链表只有一个节点,直接删掉
2、整个链表有多个节点,要删除尾节点:先顺序遍历到尾结点前一个节点,然后直接指向None
3、整个链表有多个节点,要删除中间的某个:
对于这种情况,我们将要删除节点的下一个节点的值和指针赋给该节点,然后再把下一个节点删掉;此时原本要删除节点就“摇身一变”,变成了它的下一节点。需要注意的是还要另外找一个节点做中间过渡。我们以下图删除中间的节点3为例:

代码

有了以上的分析,代码就好写了。

# offer16-solution
class Solution:
    def delete_node(self, head_node, del_node):
    
        if not (head_node and del_node):
            return False

        # 要删除的节点不是尾节点
        if del_node.next_node:
            # 先找到要删除的节点的下一个节点
            del_next_node = del_node.next_node
            # 再将下一个节点的值和指针都赋给该节点
            del_node.value = del_next_node.value
            del_node.next_node = del_next_node.next_node
            # 最后删除下一节点,此时下一节点已被当前节点所取代
            del_next_node.value = None
            del_next_node.next_node = None

        # 链表只要一个节点,删除头节点(也是尾节点)
        elif del_node == head_node:
            head_node = None
            # 不写这行可能不AC
            del_node = None

        # 链表中有多个节点,删除尾节点
        else:
            node = head_node
            # 遍历到尾结点的前一节点,然后直接指向None
            while node.next_node != del_node:
                node = node.next_node
            node.next_node = None
            # 不写这行可能不AC
            del_node = None

        return head_node

链表中的倒数第k个节点

offer19要求链表中的倒数第 k k k个节点,最无脑的方法自然是先遍历一次链表得到 n = l e n g t h ( l s t ) n=length(lst) n=length(lst),然后再一次遍历找到第 n − k + 1 n-k+1 nk+1个节点。
有没有一次遍历就搜到倒数第 k k k个节点的方法呢?答案是双指针。多个指针和链表搭配的题目最近越来越常见,但是有些题目比较新颖,第一眼不一定能找到思路,不过只要多积累多看,其实也就那么回事。
就本题而言,我们设置两个指针,让第一个指针先走 k k k步,然后两个指针同时前进直到第一个指针到达链表尾部,那么第二个指针就指向链表中的倒数第 k k k个节点了。

代码自然也不难

# offer19-solution
class Solution:
    def FindKthToTail(self, head, k):
        if not head or k <= 0:
            return None
        p1head = p2head = head
        for i in range(k-1):
            if p1head.next:
                p1head = p1head.next
            else:
                return None
        while p1head.next:
            p1head = p1head.next
            p2head = p2head.next
        return p2head

环的入口

分析

offer20要求寻找链表中是否存在环,如果存在,还要输出环的入口。
我们刚才已经初步尝试过双指针了,如链接2所言,双指针类问题大体分为左右指针和快慢指针两种。offer19是简化版的左右指针,寻找环的问题就要考虑快慢指针了。
我们先思考一下小时候经常遇到的追及问题。有两个小孩在跑步,他们的速度自然是不一样的。如果跑道是纯粹的直线,那么两人距离只会越来越远。但如果跑道是环形跑道,那快的小孩总能追上慢的小孩。即便是在环形跑道前加一段直线跑道也是一样的道理。

那么对于存在环的链表,我们也给两个指针,快指针每次向前走两步,慢指针每次向前走一步。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。
为了不失一般性,我画了一个直线跑道比环形跑道更长的链表(这样一来快指针在环里就会走不止一圈),假设这个链表的前5个节点是线型的,6是环的入口,6-9节点形成一个环,如下图我们可以看到:

第一阶段:慢指针S走到环的入口,此时走了6步,快指针F走了12步,则快指针先走完了直线跑道6+一个环4+新的步数2;此时F会停在节点8的位置(这个大家在心中模拟一下)
第二阶段:追及。S从6到8的时候,F就会从8→6→8。此时两指针相遇,这就说明链表中有环。

在解决第一个问题以后,我们还要找到环的入口,这其实是一个数学问题:我们设线型链表的长度为 L L L,环形链表的长度为 R R R,慢指针走的步数为 k k k,快指针就是 2 k 2k 2k了。再设快慢指针相交的节点距离环的入口长度为 x x x,那我们可以列两条等式:
{ k = L + x 2 k = L + n R + x ( n ∈ N ∗ ) \left\{\begin{array}{c} k=L+x \\ 2 k=L+n R+x\left(n \in N^{*}\right) \end{array}\right. {k=L+x2k=L+nR+x(nN)
消掉 k k k,有 L = n R − x ( n ∈ N ∗ ) L=n R-x\left(n \in N^{*}\right) L=nRx(nN),左边的意义非常明显,右边的 n R − x nR-x nRx代表“还差 x x x步就走完 n n n圈”,考虑慢指针现在的状况——距离环的入口有 x x x步,也就是说,如果这时候有个新指针从起点出发,逐步遍历,当它走完 L L L步到达环的入口的时候,慢指针也将同时回到环的入口。

代码

通过如上的分析我们就可以写代码了:
第一阶段:通过构造快慢指针,判断是快指针先到达None,还是两指针先相遇。前者意味着没有环,后者意味着有环。
第二阶段:如果有环,从两指针相遇的时间节点开始,让一个新的慢指针从头开始遍历,当新旧慢指针第一次相遇时,那就是命运的十字路口——环的入口。

# offer20-solution
class Solution:
    def EntryNodeOfLoop(self, pHead):
        pFast = pHead
        pSlow = pHead
        while pFast!=None and pFast.next!=None:
            pFast = pFast.next.next
            pSlow = pSlow.next
            #两个指针相遇且非空,则说明有环
            if pFast == pSlow:
                break
        if pFast == None or pFast.next == None:
            return None
            
        pFast=pHead  
        while (pFast != pSlow):
            pFast = pFast.next
            pSlow = pSlow.next
        #返回环的入口节点
        return pFast

合并有序链表

思路其实不少,包括遍历的思路。不过我觉得挺容易想到的是利用链表的有序性:比较list1 list2的头节点大小,取比较小的链表作为新链表的起点。依次取小的节点往该链表中插入元素。这个过程最好写成递归。

# offer22-solution
class Solution:
    def ListMerge(self, pHead1, pHead2):
        if not pHead1:
            return pHead2
        elif not pHead2:
            return pHead1
        pMHead = None
        if (pHead1.val < pHead2.val):
            pMHead = pHead1
            pMHead.next = self.ListMerge(pHead1.next, pHead2)
        else:
            pMHead = pHead2
            pMHead.next = self.ListMerge(pHead1, pHead2.next)

        return pMHead

能想到递归的话,这题的难度比前三个都要低一些。

参考

python数据结构----单链表删除指定元素
双指针技巧汇总
算法一招鲜——双指针问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值