代码随想录算法训练营第四天| 24. 两两交换链表中的节点、19.删除链表的倒数第N个节点、面试题 02.07.链表相交、142.环形链表II、总结|Sundri

24. 两两交换链表中的节点

总结:这个题目并不简单!!!我总是对一些简单题没有理解到足够深刻,导致很快就把这个题目做完之后,稀里糊涂的就跳过了,实际上真正搞懂搞透彻这道题,足足花了我三天的时间!

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

# 初步思考:

1.是否需要dummy node?

需要,因为我们原来的头节点变成了第二个节点,说明指向头节点的地址被更改了,因此我们肯定需要从头节点前面开始进行操作,因此推断出我们需要创建一个dummy node。

2. cur指针应该指向head还是指向dummy node?

因为题目要达到这样的效果,所以我们必须要让cur指针指向dummy node。

(0)定义虚拟头节点 dummy,定义遍历指针 cur

(1)重新定义第一个点:虚拟头节点指向2 (注意代码的顺序也一定要按照这个顺序来)

(2)重新定义第二个点:2 指向 1

(3)重新定义第三个点:1 指向 3 

(4)cur 节点向后移动到1的位置,重复1,2,3,4步的操作

3. 应该存储哪些变量值? 

(1)指向1的指针: cur.next 

(2)指向2的指针:cur.next.next

(3)指向3的指针:cur.next.next.next 

4. 这个题我怎么保证能够下次在不记得的情况下,再次写对呢?

一开始我看了很多人的代码,要么是用一个pre,一个cur指针来遍历;要么是记录下来cur.next以及cur.next.next.next,即第一个和第三个节点的内存地址。但是这两种方法都不够直观。我先写一种笨蛋都看得懂的写法,把这种写法掌握之后再优化成存储第一个和第三个节点内存地址的写法。

顺序口诀:从头到尾确定节点

先确定第一个节点,再确认第二个, 再确认第三个,以此类推

first = cur.next

second = cur.next.next

third = cur.next.next.next 

while cur.next and cur.next.next:

cur.next = second # 确认第一个节点:dummy node  --> 2 

cur.next.next = first # 确认第二个节点:2 --> 1

cur.next.next.next = third # 确认第三个节点:1 -->3 

# 解题代码:

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0)
        dummy.next = head
        cur = dummy
        while cur.next and cur.next.next:
            third = cur.next.next.next # 指向3位置的指针
            second = cur.next.next # 指向2位置的指针
            first = cur.next # 指向1位置的指针

            cur.next = second # 第一个节点变成2
            cur.next.next = first # 第二个节点变成1
            cur.next.next.next = third # 第三个节点变成3

            cur = cur.next.next # cur移动到1的位置,继续重复上述过程

        return dummy.next

# 编译错误原因:

1.忘记返回dummy.next,导致答案是空

2.没有储存第一个节点和第三个节点的地址,导致传递参数时出错

3.更改的顺序也有讲究,如果是先1->3,然后2-1,然后头到2,也会出错 (order is the key!)

# 优化之后的代码:

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        dummy = ListNode(0)
        dummy.next = head
        cur = dummy
        while cur.next and cur.next.next:
            third = cur.next.next.next # 指向3位置的指针
            first = cur.next # 指向1位置的指针

            cur.next = cur.next.next # 第一个节点变成2
            cur.next.next = first # 第二个节点变成1
            cur.next.next.next = third # 第三个节点变成3

            cur = cur.next.next # cur移动到1的位置,继续重复上述过程

        return dummy.next

19. 删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz

进阶:你能尝试使用一趟扫描实现吗?

# 核心思路:

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

# 易错点思考:

1.什么时候需要使用dummy node,什么时候不需要使用dummy node?

只要涉及到链表删除的操作:一定要使用dummynode,因为会存在删除头节点的情况。使用dummy node可以很好的涵盖这种情况的实现。

2.while循环的遍历条件应该怎么写,什么时候是 while fast 什么时候是 while fast.next?

这个题目里面,最远的位置是 fast.next,为了保证fast.next有值,所以要写while fast and fast.next (我试过,如果写成 while fast.next 程序也不会报错!!! )所以技巧就是:找到跑的最远的指针,while条件里面对这个条件进行判断。

比如这段代码里,程序段出现的语句是 slow = slow.next; fast = fast.next,这就要求 slow.next 和 fast.next都不为空,程序才可以执行下去。而又因为slow此时跑的比fast要慢,所以只要fast指针满足条件,slow指针就一定满足条件。

3.slow和fast指针的初始位置应该定义在哪里?

因为有可能删除的是头节点,所以要在头节点前一位去找!因此定义在dummy!

# 代码实现: 

def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        if not head: return

        dummy = ListNode(0)
        dummy.next = head 
        slow, fast = dummy, dummy   
        for _ in range(n):
            fast = fast.next 
        while fast and fast.next:
            slow = slow.next 
            fast = fast.next 
        slow.next = slow.next.next 
        return dummy.next 
    

链表相交

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

示例 1:

# 易错点:

1.为什么不需要创建dummy head?

因为这个题目不涉及到指针的删除,因此不需要跟踪头节点的位置,也就不需要dummy

2.while判断条件为什么是 while p != q, 为什么最后这样写还能在不相交的情况下跳出循环?

因为这三句非常精髓的话:

while p != q:

        p = headB if p == None else p.next

        q = headA if q == None else q.next

return p

1.如果p和q不存在相交的情况,那么p和q都会走到空指针位置停下来,最后无论是返回p还是q,结果都会是空指针。

2.如果p和q存在相交的情况,那么p和q就会在相交处停下来,最后无论是返回p还是返回q都会是相交点。

 142.环形链表

给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

链表中节点的数目范围在范围 [0, 104] 内
-105 <= Node.val <= 105
pos 的值为 -1 或者链表中的一个有效索引

进阶:你是否可以使用 O(1) 空间解决此题?

# 解题关键:找环的入口

1. 先找到相遇点

2. 接着在相遇点放一个指针,在头节点放一个指针

3. 两个指针同时向前移动,直到相遇的时候,就是环的入口

如果有环,如何找到这个环的入

假设从头结点到环形入口节点 的节点数为x。
环形入口节点到 fast指针与slow指针相遇节点 节点数为y。
从相遇节点 再到环形入口节点节点数为 z。 如图所示:

142环形链表5

 

那么相遇时:
slow指针走过的节点数为: x + y
fast指针走过的节点数: x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为我们要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以我们要求x ,将x单独放在左面:x = n (y + z) - y

在从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针

这个公式说明什么呢,

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # if not head or not head.next: return
        slow, fast = head, head 
        while fast and fast.next:
            slow = slow.next 
            fast = fast.next.next 
            if slow == fast:
                p = slow 
                q = head
                while p != q:
                    p = p.next 
                    q = q.next 
                return p

        return None

1.slow和fast指针应该从dummy开始还是head开始遍历?

因为不涉及到指针的移动和删除,因此选择从head开始遍历就可以了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值