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

LeetCode 24.两两交换链表中的元素:

文章链接
题目链接: 24.两两交换链表中的元素

看到题目后的想法:

① 交换两个节点不是单纯改变两个节点的值,需要进行实际的节点交换
② 如果只用到待交换的两个节点cur1, cur2,那么这样交换完成之后cur1 的pre节点实际上还是指向cur1的,没有指向cur2。因此交换时一共需要三个指针pre,cur1 和 cur2。为了方便统一操作,采取添加虚拟头节点的方式实现。
③ 交换节点的方式如下:其中①必须在②前
在这里插入图片描述
交换完成后:
在这里插入图片描述

代码实现

  1. 迭代
    使用虚拟头节点的迭代
    需要注意的是先对pre.next和pre.next.next进行是否为空,再使cur1和cur2指向要交换的两个节点和交换节点。先指向的话,会出现求None.next的情况
# 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
        """
        dummy_head = ListNode(next = head)  # 创建虚拟头节点
        pre = dummy_head
        while pre.next != None and pre.next.next != None:   # 先判断交换的节点是否存在,再进行交换
            cur1, cur2 = pre.next, pre.next.next
            # 交换
            cur1.next = cur2.next
            cur2.next = cur1
            pre.next = cur2
            # 更新pre
            pre = cur1
        return dummy_head.next
  1. 递归
    不使用虚拟头节点的递归实现。
    递归中的节点交换:首先让cur2.next指向cur1,再对以cnext为头节点的链表进行两两交换,得到交换后链表的新头节点cnext1, 从而cur1.next = cnext1
class Solution(object):
    def swapPairs(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        if head == None or head.next == None:
            return head
        cur1, cur2 = head, head.next    # 不带头节点的链表的交换
        cnext = cur2.next   # 下一次递归要交换的链表的第一个节点
        # 交换cur1和cur2
        cur2.next = cur1
        cur1.next = self.swapPairs(cnext)   # 将以cnext为头节点的链表进行两两交换
        return cur2

实现过程中遇到的困难:

① 要先在纸上对节点交换进行模拟,分析清楚要交换的节点以及指针操作的先后顺序
② 在具体代码实现中,要先对交换的两个节点进行判断是否为空,再使得指针指向两个节点以及交换节点


LeetCode 19.删除链表的倒数第N个节点:

文章链接
题目链接:19.删除链表的倒数第N个节点

看到题目的想法

  1. 暴力求解
    考虑到会出现删除第一个元素的情况,所以添加虚拟头节点dummy_head。
    ① 遍历链表,统计链表长度size。
    ② 从dummy_head开始遍历size - n 次,n从1开始,得到待删除节点的的pre。
    ③ 删除节点
  2. 双指针解法
    ① 采用快慢指针求解,因为题目要求是倒数第k个节点,即快慢指针之间的距离是相同的。因此采用初始位置不同,步长均为1的快慢指针。
    ② 考虑到会出现删除第一个节点的情况,对原链表添加虚拟头节点。
    ③ 快慢指针之间的距离:
    采用删除最后一个节点测试是比较好的方法:
    1)如果最后fast停在了尾节点的位置,slow和fast之间的距离为1,此时n为1。因此slow和fast之间的距离为n
    在这里插入图片描述
    2)如果最后fast停在了NULL,slow和fast之间的距离为2,此时n为1。从而slow和fast之间的距离为n + 1
    在这里插入图片描述
    代码部分(fast到尾节点的方式)
# class ListNode(object):
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution(object):
    def removeNthFromEnd(self, head, n):
        """
        :type head: ListNode
        :type n: int
        :rtype: ListNode
        """
        dummy_head = ListNode(next = head)
        slow = fast = dummy_head
        for i in range(n):  
            fast = fast.next    # 快指针比慢指针快n步
        # 移动两个指针,直到快指针到尾节点
        while fast.next != None:
            slow = slow.next
            fast = fast.next
        # 通过更新倒数第(n - 1)个节点的next来删除第n个节点
        slow.next = slow.next.next
        return dummy_head.next

实现过程中遇到的困难:

对于快慢指针的距离,可以采用举例的方式来进行确定(因为一定是n ± k)。
假定n = 1,那么遍历完成后slow为倒数第2个节点,此时根据fast的位置确定slow和fast之间的距离


面试题 02.07.链表相交:

文章链接
题目链接:02.07.链表相交

看到题目后的想法:

使用两个指针curA和curB遍历A链表和B链表。当curA==curB时,表明找到了相交的节点。问题出现再curA != curB时,两者怎么移动的问题。
假定A链表比B链表短,解决办法如下

  1. 从对齐的节点开始遍历
    先让curB移动 len(B) - len(A)的距离,使得curA和curB对齐,从而遍历过程中当curA != curB时,curA和curB可以同时向后移动。
    因为移动出现问题的原因在于A和B的长度不相同,那么移动curB后即在A和curB为第一个节点的链表中寻找相交节点,此时这两个链表的长度是相同的。
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None

class Solution(object):
    def getIntersectionNode(self, headA, headB):
        """
        :type head1, head1: ListNode
        :rtype: ListNode
        """
        dis = self.getListLen(headA) - self.getListLen(headB)   # 求距离差
        # 通过移动较长的链表,使两个链表相等
        if dis < 0:
            curB = self.movHead(headB, abs(dis))
            curA = headA
        else:
            curA = self.movHead(headA, dis)
            curB = headB
        while curA != curB:
            curA = curA.next
            curB = curB.next
        return curA
        
    def getListLen(self, head): # 求链表长度
        cur, l = head, 0
        while cur != None:
            l += 1
            cur = cur.next
        return l
    def movHead(self, head, dis):   # 将链表节点移动指定长度
        cur = head
        for _ in range(dis):
            cur = cur.next
        return cur     
  1. 均从第一个节点开始遍历
    与上面的思路相似,不同在于思路1是先得到两个链表长度的差,再移动curB。而思路2在于,同时从A和B的头节点开始遍历,当curA(curB)遍历到链表A(B)末尾后,切换到另一个链表B(A)的头节点开始遍历。
    这种思路的原理在于,第一遍遍历完成之后curA和curB已经相差了len(B) - len(A),从而在第二遍遍历时curA和curB变成了对齐的。(暂且先这么解释)
class Solution(object):
    def getIntersectionNode(self, headA, headB):
        """
        :type head1, head1: ListNode
        :rtype: ListNode
        """
        curA, curB = headA, headB
        # 遍历两个链表直到指针相交
        while curA != curB:
            # 将指针向前移动一个节点
            curA = curA.next if curA else headB # 只有一个到达末尾才切换链表  
            curB = curB.next if curB else headA
        # 如果相交,curA为相交节点;否则为None
        return curA  

实现过程中遇到的困难:

对于第二种思路还需要更细致深入的理解


LeetCode 142.环形链表Ⅱ:

文章链接
题目链接: 142.环形链表Ⅱ

看到题目后的想法

  1. 暴力。
    ① 因为比较节点是否相等需要同时用到val和next,而新链表对已经遍历过的节点进行记录的顺序和原链表相同,因此新链表采用的方法是:创建虚拟头节点指向head,dsize为新链表的节点个数,加入节点的方式为dsize + 1
    ② 对原链表进行遍历,遍历过程中将节点加入新链表中,每次遍历到一个节点,都将其与新链表中的节点进行对比,查看在之前是否出现过。
    ③ 但是时间复杂度O(n^2), 空间复杂度O(1)
# 力扣上面可以过,时间很长
# 简单写一下代码的结构
# class ListNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.next = None
class Solution(object):
    def detectCycle(self, head):
        dummy_head = ListNode(next = head)
        dsize = 0
        cur = head
        while cur != None:
            dcur = dummy_head
            for i in range(dsize):
                dcur = dcur.next
                if dcur == cur:
                    return cur
            dsize += 1
            cur = cur.next
        return None

不知道算不算优化方法:使用python中的集合来降低时间复杂度。每次遍历判断节点是否在集合中,不在将其加入集合中

class Solution(object):
    def detectCycle(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        visited = set()
        cur = head
        while cur != None:
            if cur in visited:
                return cur
            visited.add(cur)
            cur = cur.next
        return None
  1. 快慢指针
    首先清楚我们要解决的问题:① 是否存在环 ② 找到环的入口
    ① 是否存在环: fast和slow指针从同一个位置开始,fast的步长为2,slow的步长为1。如果存在环,那么fast和slow会在环中相遇。
    直观解释
    假定链表中存在环,那么slow进入环时,fast一定已经在环的某个位置。为了便于理解,我们假定slow的位置比fast快 k 步, k < len(环)。
    在每个时间间隔中,fast相对于slow多走的步数为1,那么经过k个时间间隔,fast一定会赶上slow。(类似物理中的相对速度和相对位移)

在这里插入图片描述
② 怎么找到环的入口:入口与head到entry的距离有关
在这里插入图片描述
相遇时,slow走过的节点数为x + y;fast走过的节点数为x + y + n * (y + z)。其中n >= 1(fast至少经过了两次entry)。
而slow和fast的速度比为1:2,因此2 *(x + y) = x + y + n * (y + z),从而得到 x = (n - 1) * (y + z) + z。
直观上理解就是,如果有两个指针p1和p2分别从head和相遇的节点出发,那么p1走x步到达entry;p2也走x = (n - 1) * (y + z) + z,先走 n - 1个环的长度回到相遇的节点(周期),再走z步也到达entry。
所以p1和p2同时出发,步长为1,最终一定会相遇且相遇的节点为入口节点。从而找到了入口节点的位置

class Solution(object):
    def detectCycle(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        fast = slow = head
        while fast != None and fast.next != None:
            fast = fast.next.next
            slow = slow.next
            # 如果存在环的话,那么fast和slow一定会相遇
            if fast == slow:
                # 将其中一个移动到链表开始位置
                fast = head
                while fast != slow: # 新的相遇位置为entry
                    fast = fast.next
                    slow = slow.next
                return fast # 返回入口节点
        # 不存在环,返回None
        return None 

实现过程中遇到的问题:

步长不相同的快慢指针在移动时,判断条件为fast != None和fast.next != None(第一个是fast为循环中判断的对象,因此需要保证fast存在;第二个保证fast.next存在,从而不会出现fast.next.next = None.next的情况)


学习收获:

① 对于步长不相同的快慢指针有了进一步的了解。自己实现代码的过程中明确了一些技术细节
② 对于比较复杂的指针/模拟,先在纸上手动模拟再写代码是更好的选择,可以降低代码出错的可能性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值