【刷题笔记day4】链表进阶题目练习

LC24 两两一组反转链表

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
例如:Input: head = [1,2,3,4] → Output: [2,1,4,3]

本题有两种解法:迭代法和递归法。

迭代法

这个方法涉及比较多的指针操作,很容易绕晕,但在开始绕之前,需要明白以下知识点:

  • 要想对相邻节点A和B进行反转,我们需要一个指针指向A的上一个节点。这是开始反转操作的大前提。
  • 链表节点数可能是奇数个或偶数个,这个条件直接影响循环终止条件(详见代码)

【刷题笔记day4】链表进阶题目练习_第1张图片

现在来看代码(借用代码随想录的图以方便理解)

class Solution:
     def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
         """
         迭代法
         """
         if not head:
             return
         dummy = ListNode(next = head)
         cur = dummy
        
         while cur.next and cur.next.next:
             # 首先用指针分别指向 target pair 的前一个节点和后一个节点
             temp1 = cur.next
             temp2 = cur.next.next.next
             # 以下三步反转 target pair
             # 与上图的顺序有差别
             cur.next = cur.next.next  # “步骤一”
             temp1.next = temp2        # “步骤三”
             cur.next.next = temp1     # “步骤二”
             # 移动cur,为下一次循环做好准备
             cur = cur.next.next
            
         return dummy.next

递归法

掌握本题的递归解法更有助于解答下一题: LC25 K个一组反转链表

class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        """
        递归法
        """
        if not head or not head.next:
            # 如果奇数个节点,传入的head是None
            # 如果是偶数个,传入的head.next是None
            return head
        
        first = head
        second = head.next
        others = head.next.next
        
        # 先反转前两个元素
        second.next = first
        # 利用递归定义,将剩下链表节点两两反转,接到后面
        first.next = self.swapPairs(others)
        # 现在整个链表已经成功反转,返回新的头节点
        return second

通过对比两种解法可以发现,迭代法是“自底向上”地找出答案;而递归法是“自顶向下”地反推,相信我们都能想到数学归纳法:

转自 从“数学归纳法”到理解“递归算法”!

  1. 证明基本情况(通常是N = 1 的时候)是否成立。 证明对于N=1成立。我们只需要先从最小的自然数开始证明。这一步通常非常简单。关键是证明第二步。
  2. 证明N > 1 时,假设 N - 1 成立,那么对于N成立(N为任意大于1的自然数)。
    这一步并不是直接证明的,而是假设N-1成立,利用这个结论推出N是成立的。如果能够推出的话,就可以说:对于所有的自然数都成立。因为证明了对1成立,那么对2成立,对3也成立。那么就证明了对所有自然数都成立。

所以要对原链表(假设有6个节点)两两一组反转,在反转1号和2号之前,要先确保3号到6号已经反转好了,在反转3号4号前,要确保5号6号已经反转好了。上面的递归函数中的first.next需要接住的就是当前问题的子问题的答案,也就是我上一句话描述的过程。

拓展题: LC25 K个一组反转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        # base case
        if head == None: 
            return None
            
        # 确定反转区间,a, b是反转区间的头尾节点
        a, b = head, head
        for i in range(k):
            # 如果区间不足k个,则保持原有顺序返回
            if (b == None): return head
            b = b.next
            
        # 反转区间
        newHead = self.reverse(a, b)
        # 递归到下一层
        a.next = self.reverseKGroup(b, k)
        return newHead
    
    # 迭代法反转子链表
    def reverse(self, a: Optional[ListNode], b: Optional[ListNode]) -> Optional[ListNode]:
        pre = None
        cur = a
        nxt = a
        
        while cur != b:
            nxt = cur.next
            cur.next = pre
            pre = cur
            cur = nxt
        return pre

LC19 删除链表倒数第N个节点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?

对于单链表,要一趟扫描找到倒数第n个节点,需要借助快慢双指针技巧,来实现两个循环才能达到的效果。

简而言之就是快指针先前进n步,然后慢指针和快指针保持相对静止,二者都做匀速运动直到快指针运动到最后一个节点时,慢指针就指向了倒数第n个节点。

那么要删除这个节点,我们需要一个虚拟头节点以便返回最终结果,还需要让慢指针最终运行到倒数第n+1个节点,也就是倒数第n个节点的前一个节点,这样方便进行删除操作。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        dummy = ListNode(next=head)
        p1, p2 = dummy, dummy
        # p1 前进n + 1步
        for i in range(n + 1):
            p1 = p1.next
            
        # 结束循环时,控制p2指向要删除节点的上一个节点
        while p1 != None:
            p1 = p1.next
            p2 = p2.next
        p2.next = p2.next.next
        return dummy.next

LC160 相交链表

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

两个链表长度不一怎么办?分别把各自接到对方的后面去,就形成了逻辑上两条长度一样的链表。这样子当从头开始遍历的时候,两指针相等之时就可以证明两条链表相交。

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

class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
        p1, p2 = headA, headB
        while p1 != p2:
            if p1 != None:
                p1 = p1.next
            else:
            	# 遍历到尾部了,从另一条链表的头部继续
                p1 = headB
            if p2 != None:
                p2 = p2.next
            else:
            	# 遍历到尾部了,从另一条链表的头部继续
                p2 = headA
                
        # 假如没有交点,p1, p2 最后都会为None,循环退出
        return p1

LC142 环形链表II

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

本题是LC141 环形链表的进阶版,后者只需要判断链表中是否有环,属于本题解答的一部分,所以合并过来。

本题的思路是快慢双指针,二者同时出发,但快指针一次运动两步,慢指针一次运动一步,假如存在环,那么快指针一定会追上慢指针(想起来中学的1000米考试)这就是LC141的解法。

但本题还需要确定环的起点,这个就需要一点数学功底了。先放结论:

当快慢指针第一次相遇时,让快指针重新从起点出发,但此时二者速度相同,那么二者再次相遇地点就是环的起点

证明过程(来自代码随想录)

那么相遇时: 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同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

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

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if head == None:
            return None
        slow, fast = head, head
        
        # 判断是否存在环
        while fast != None and fast.next != None:
            fast = fast.next.next
            slow = slow.next
            if slow == fast:
                break
        # 这里要明确是什么原因导致循环退出
        # 如果是fast遇到空指针,那就说明没有环
        if fast == None or fast.next == None:
            return None
            
        # 找环的起点
        fast = head
        while fast != slow:
            fast = fast.next
            slow = slow.next
        return slow

参考

如何K个一组反转链表

你可能感兴趣的:(LeetCode刷题笔记)