​LeetCode刷题实战19:删除链表的倒数第N个节点

共 3062字,需浏览 7分钟

 ·

2020-08-24 02:55

算法的重要性,我就不多说了吧,想去大厂,就必须要经过基础知识和业务逻辑面试+算法面试。所以,为了提高大家的算法能力,这个公众号后续每天带大家做一道算法题,题目就从LeetCode上面选 !


今天和大家聊的问题叫做删除链表的倒数第N个节点 ,我们先来看题面:

https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/

Given a linked list, remove the n-th node from the end of list and return its head.

题意


给定一个链表,要求移除导数第n个元素,并且返回新链表的head
说明:给定的 n 保证是有效的。

样例


给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5.


Follow up:
Could you do this in one pass?
你能一发通过么?官方嘲讽。

题解

题目的意思非常直白,不需要太多思考就能理解。但是上手去做的话会有一点小问题,因为如果是数组很好办,我们直接可以求到数组的长度,导数第N个元素也非常容易确定。但是如果是链表的话就不同了,因为我们并不知道链表的长度,所以没办法直接获取需要删除节点的位置
既然直接没有办法求到,那么可以间接去求嘛,所以很自然地可以想到两次遍历的方法。

两次遍历

这个方法非常直观,基本上可以说是顾名思义。我们对这个链表遍历两次,第一次求到链表的长度,这样我们就可以推算到倒数第N个数是正数第几个数了。第二次我们移动对应的长度,找到需要删除的节点,将它移除即可。
我们来看下面这张图,完全可以脑补出算法:


虽然算法不难,但是这题当中藏着trick,隐藏了出题人深深的恶意。不相信的话,可以试着不看答案写写看,基本上一定会错上一两次。原因很简单,因为会有一些特殊情况需要考虑。
比如,特殊情况1:链表当中只有一个元素,显然这个时候根本不需要移动,也不用删除,直接return None就好了。但是如果我们使用常规方法的话,是无法删掉的,必须要特殊判断这种情况。
特殊情况2:这个要删的元素刚好是第一个head元素,这种情况也没有办法常规解决,也需要特殊判断。
把这两个特殊情况考虑到,基本上就没问题了。

class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        l = 0
        pnt = head
        # 计算链表长度
        while pnt:
            l += 1
            pnt = pnt.next
        # 如果长度为1,直接return None
        if l == 1:
            returnNone
        # 计算删除需要移动的长度
        l = l - n - 1
        # 如果小于0,说明需要删除第一个元素,那么直接return head.next。
        if l < 0:
            return head.next
        pnt = head
        for i in range(l):
            pnt = pnt.next
        pnt.next = pnt.next.next
        return head

一次遍历

上面的这种算法中规中矩,基本上没有难度。那么有没有更好的算法,比如我们能不能做到只遍历链表一次呢?这样不就优化了复杂度了吗?
当然是可以的,说来这种算法也非常巧妙。如果说我们把链表想象成一条跑道,而把移动遍历的指针想象成跑道上的运动员。我们现在不知道跑道有多长,但是我们想要知道距离终点30米的位置。我们还知道运动员的奔跑速度都一样。应该怎么做呢?
我们可以先让一个运动员先跑30米,然后再派另外一个运动员从起点出发。这样,当第一个运动员跑到终点的时候,第二个运动员所在的位置就是距离终点30米的位置。
我们把上述的运动员换成指针,跑道换成链表,就是这题的解法了。
同样,我们也有一张图可以说明:


我们根据描述,可以试着写出代码。

class Solution:
    def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
        pnt1 = head
        
        # 第一个指针先跑n步
        for i in range(n):
            pnt1 = pnt1.next
            # 注意,如果是删除head,会出现跑过头的情况,需要判断
            if pnt1 isNone:
                return head.next
            
        pnt2 = head
        # 移动第一个指针,直到结尾
        while pnt1.next:
            pnt1 = pnt1.next
            pnt2 = pnt2.next
        
        # 删除pnt2.next位置的节点
        pnt2.next = pnt2.next.next
        return head

看起来第二种方法似乎更优一些,但实际上如果分析复杂度的话,两者都是的复杂度,并没有高下之分。而且比较奇葩的是,我用CPP提交的时候是能明显感觉出来第二种方法优于第一种几毫秒的,但是当我换成了Python之后,第二种方法的耗时反而还更长了。可见,快慢都是相对的,一般情况下来说在复杂度相同的情况下,优化常数意义不大。
虽然搞了半天,并没有什么效果,但至少我们明白了这点,也算是收获。
好了,今天的文章就到这里,如果觉得有所收获,请顺手点个在看或者转发吧,你们的支持是我最大的动力。

上期推文:


LeetCode刷题实战1:在数组上遍历出花样

LeetCode刷题实战2:用链表模拟加法

LeetCode刷题实战3:最长不重复子串

LeetCode刷题实战4:两个正序数组的中位数

LeetCode刷题实战5:判断回文子串

LeetCode刷题实战6:Z字形变换

LeetCode刷题实战7:整数反转

LeetCode刷题实战8:字符串转换整数

LeetCode刷题实战9:求解回文数

LeetCode刷题实战10:字符串正则匹配

LeetCode刷题实战11: 盛最多水的容器

LeetCode刷题实战12: 整数转罗马数字

LeetCode刷题实战13: 罗马数字转整数

LeetCode刷题实战14: 最长公共前缀

LeetCode刷题实战15:三数之和

LeetCode刷题实战16: 最接近的三数之和

LeetCode刷题实战17: 电话号码的字母组合

LeetCode刷题实战18: 四数之和


浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报