(补)算法训练Day4 | LeetCode24. 两两交换链表中的节点(模拟);19.删除链表的倒数第N个节点(双指针之快慢指针);160. 相交链表;142. 环形链表(数学推理)

发布于:2022-12-02 ⋅ 阅读:(766) ⋅ 点赞:(0)

目录

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

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考

LeetCode19.删除链表的倒数第N个节点

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考

Leetcode160. 相交链表

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考

LeetCode142. 环形链表

1. 思路

 2. 代码实现

3. 复杂度分析

4. 思考


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

链接: 24. 两两交换链表中的节点 - 力扣(LeetCode)

1. 思路

这道题目正常模拟就可以了。

建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。

接下来就是交换相邻两个元素了,此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序

初始时,cur指向虚拟头结点,然后进行如下三步:

操作之后,链表如下:

看这个可能就更直观一些了:

 具体实现的细节:

  1. 创建dummyNode,避免把链表头节点拿出来单独讨论;
  2. cur要指向需要操作的两个链表Node的前一个元素,才能对后面两个Node进行操作,所以初始化的cur指向dummyNode即可;
  3. while的终止条件是什么?链表节点两两交换,如果链表的节点个数是偶数个,那么终止条件是cur.next == None , 如果链表的节点个数是奇数个,那么终止条件是cur.next.next == None;
  4. 交换节点的具体操作,在实施的时候会发现,需要把cur.next 以及cur.next.next.next 都用临时指针tmp保存起来,不然需要的节点会在改变指向的时候丢失;

2. 代码实现

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution(object):
    def swapPairs(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        dummyNode = ListNode(0)
        dummyNode.next = head
        cur = dummyNode
        while(cur.next != None and cur.next.next != None):
            # 先记录必要的节点
            tmp1 = cur.next
            tmp2 = cur.next.next.next
            # 交换节点指针的逻辑
            cur.next = cur.next.next # 步骤一
            cur.next.next = tmp1  # 步骤二
            tmp1.next = tmp2  # 步骤三
            # cur 向后移动
            cur = cur.next.next 
        return dummyNode.next

3. 复杂度分析

时间复杂度:O(N)

cur指针需要把链表从头遍历到尾,几乎每个节点都需要进行一次交换的逻辑,所以时间复杂度为O(N);

空间复杂度:O( 1)

空间上只有几个指针占用内存,是常数级别的,因此空间复杂度为O(1)。

4. 思考

  1. 链表的很多题目几乎都要用到虚拟头节点的技巧,可以精简代码;
  2. 写出来代码之后,需要在心里想一下极端情况,比如链表为空,链表只有一个元素的情况,代码能不能cover住;
  3. 本题链表操作相对复杂,但是核心就主要是两个问题要想清楚,第一个是while的循环条件是什么,第二个是交换元素的操作是什么样的。

Reference:

代码随想录 (programmercarl.com)

本题学习时间:40分钟。


LeetCode19.删除链表的倒数第N个节点

链接: 19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

1. 思路

这一题的关键点是如何找到倒数第N个节点(倒数第N个节点-1)?

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

思路是这样的,但要注意一些细节。分为如下几步:

  • 首先使用虚拟头结点,这样方面处理删除实际头结点的逻辑,
  • 定义fast指针和slow指针,初始值为虚拟头结点,如图:

  • fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:

  • fast和slow同时移动,直到fast指向末尾,如图:

  • 删除slow指向的下一个节点,如图:

2. 代码实现

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
# time:O(N);space:O(1)
class Solution(object):
    def removeNthFromEnd(self, head, n):
        """
        :type head: ListNode
        :type n: int
        :rtype: ListNode
        """
        # 双指针法
        # complexity analysis: time O(N) space O(1)
        dummyNode = ListNode()
        dummyNode.next = head
        slowPointer = dummyNode
        fastPointer = dummyNode
        for _ in range(n+1):
            fastPointer = fastPointer.next
        while fastPointer:
            slowPointer = slowPointer.next
            fastPointer = fastPointer.next
        slowPointer.next = slowPointer.next.next
        return dummyNode.next
    

3. 复杂度分析

时间复杂度:O(N)

要找到倒数第N个节点,最差的情况要一直遍历到最末尾才能找到,因此时间复杂度为O(N);

空间复杂度:O(1)

只占用常数个指针,因此空间复杂度为O(1)。

4. 思考

  1. 用next的时候要想,会不会产生None.next的情况,在这里,如果是删除的最后一个元素的话,slowPointer 会指向倒数第二个,所以slowPointer.next.next本身才是None;
  2. 考虑链表为空和链表只有一个元素的情况是否包含在代码里面了。

Reference:代码随想录 (programmercarl.com)

本题学习时间:40分钟。


Leetcode160. 相交链表

链接:160. 相交链表 - 力扣(LeetCode)

1. 思路

简单来说,就是求两个链表交点节点的指针。 这里同学们要注意,交点不是数值相等,而是指针相等

两个链表,一定有一个长的,有一个短的,如果他们在某个地方有交点,那么一定在某个地方的指针指向同一个Node,然后之后的node都是重合的。

所以可以先把两个链表右对齐,然后以对应短链表的头节点为起始点,两个指针同时遍历,然后同时比较,如果在走到最后一个元素之前,有指针相等的情况,则说明这两个链表有交点。

举个栗子

为了方便举例,假设节点元素数值相等,则节点指针相等。看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点:

 我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图:

 此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。

否则循环退出返回空指针。

2. 代码实现

# time:O(m+n);space:O(1)
class Solution(object):
    def getIntersectionNode(self, headA, headB):
        """
        :type head1, head1: ListNode
        :rtype: ListNode
        """
        LenA = 0
        LenB = 0
        curA = headA
        curB = headB 
        # 计算A和B链表各自的长度
        while curA:
            curA = curA.next
            LenA += 1
        while curB:
            curB = curB.next
            LenB += 1
        # 让A成为最长的链表
        if LenB > LenA:
            headA,headB = headB,headA
            LenA,LenB  = LenB,LenA
        # 将起始指针移动到合适的位置
        gap = LenA - LenB
        curA,curB = headA,headB 
        for _ in range(gap):
            curA = curA.next 
        # 开始同时遍历判断是否相等了
        for _ in range(LenB):
            if curA == curB:
                return curA
            else:
                curA = curA.next
                curB = curB.next
        return None

3. 复杂度分析

时间复杂度:O(m+n)

m和n各自是长链表和短链表的长度,首先计算两个链表长度的复杂度就分别是O(m)和O(n),将A的指针移动到合适的位置就是O(m-n)的复杂度,然后两个指针同时往后遍历就是O(2n)的复杂度,综上全部加起来,是O(m+n)的时间复杂度;

空间复杂度:O(1)

整个代码只有常数个变量储存,因此空间复杂度为O(1)。

4. 思考

  1. 数值相同,不代表指针相同,要定义两个指针去比较才可以,不能光看数值;
  2. 一开始觉得不知道从何开始比较,抓住一个关键点,两个链表如果有相交的元素的话,把最后面对齐之后,一个个比较即可。

Reference:代码随想录 (programmercarl.com)

本题学习时间: 30分钟。


LeetCode142. 环形链表

链接:142. 环形链表 II - 力扣(LeetCode)

1. 思路

这道题目,不仅考察对链表的操作,而且还需要一些数学运算。

主要考察两知识点:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

判断链表是否有环

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。因为在直线的阶段,fast移动速度快于slow,方向又不变,fast和slow不可能在直线上相遇。

那么来看一下,为什么fast指针和slow指针一定会相遇呢?

可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。

会发现最终都是这种情况, 如下图:

fast和slow各自再走一步, fast和slow就相遇了

这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

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

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

 

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

因为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

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

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

 那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。

其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

思路补充

理解方法一:

在推理过程中,大家可能有一个疑问就是:为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?

 首先slow进环的时候,fast一定是先进环来了。

如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:

 可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。

重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:

 那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。

因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。

也就是说slow一定没有走到环入口3,而fast已经到环入口3了

这说明什么呢?

在slow开始走的那一环已经和fast相遇了

那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去

理解方法二:

 2. 代码实现

# Definition for singly-linked list.
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

# time:O(N);space:O(1)
class Solution(object):
    def detectCycle(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        slow = head
        fast = head
        while fast != None and fast.next != None:
            slow = slow.next
            fast = fast.next.next
            # 判断是否有环,如果相遇则有环
            if fast == slow:
                meet = slow
                begin = head
                # 找环的入口
                while meet != begin:
                    meet = meet.next
                    begin = begin.next
                return meet
        return None
    

3. 复杂度分析

时间复杂度:O(N)

其中 N 为链表中节点的数目。在最初判断快慢指针是否相遇时,slow 指针走过的距离不会超过链表的总长度,fast指针走的距离不会超过二倍的长度;随后寻找入环点时,走过的距离也不会超过链表的总长度,因此,总的执行时间为O(N) ;

空间复杂度:O(1)

我们只使用了 slow,fast,meet,begin 这几个指针

4. 思考

  1. 算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口
  2. 本题的难度在于数学推理出这种做法来,其实代码写出来非常简洁。

Reference:

代码随想录 (programmercarl.com)

本题学习时间: 60分钟。


PS:本篇学习了链表的四道题,用到了模拟,双指针和数学推理的技巧;是10.2号补上的;总体所花时间3小时。