算法学习 Day04 链表基础2

159 阅读7分钟

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

文章讲解

视频讲解

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

image.png

解题思路

我们可以使用递归的方法来解决这个问题。递归的思路是:

  • 1 首先反转前面两个节点。
  • 2 然后递归地反转第三个节点开始的链表。
  • 3 最后连接第一个节点和反转后的第三个节点。。
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
class Solution:
    def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 确保有两个节点
        if head is None or head.next is None:
            return head
        
        prev = head
        cur = head.next
        nxt = head.next.next

        # 反转前两个
        cur.next = prev
        prev.next = self.swapPairs(nxt)
        return cur

总结

通过递归的方法,我们可以较为简洁地实现链表中两两交换节点的问题。递归的核心在于理解每一层递归的作用,以及如何通过递归处理剩余部分的链表。指针操作是实现过程中需要特别注意的地方,确保每一步操作都正确无误。

25. K 个一组翻转链表

文章讲解

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

image.png

解题思路

本题可以使用递归的方法,首先反转以head开头的一组,然后按照定义用reverseKGroup反转K+1节点开始的整个链表,再拼起来:

  • 1 首先判断链表是否为空或只有一个节点,如果是,直接返回头节点。
  • 2 找到第一组需要反转的节点范围,即从头节点开始,找到第 k 个节点。如果不足 k 个节点,说明无法完成反转,直接返回头节点。
  • 3 反转第一组节点,调用 reverse 函数完成反转,并获得反转后的头节点。
  • 4 递归调用 reverseKGroup 函数,处理剩余的节点。将第一组反转后的头节点与剩余部分链表连接起来。
  • 时间复杂度:O(n)
  • 空间复杂度:O(n/k),即递归调用的深度。
class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        if head is None or head.next is None:
            return head
        
        # 找到第一组的范围
        left, right = head, head
        for i in range(k):
            if right is None: # 不满k个节点,不反转,直接返回head
                return head
            right = right.next

        # 反转第一组
        newHead = self.reverse(left, right)

        # 反转后面的组并拼接
        left.next = self.reverseKGroup(right, k)
        return newHead
    
    def reverse(self, a:ListNode, b:ListNode)->ListNode:
        ''' 反转[a,b)区间内的链表 '''
        prev = None
        cur = a
        while cur != b:
            nxt = cur.next
            cur.next = prev

            prev = cur
            cur = nxt
        
        return prev # cur=b了,cur=b-1

总结

通过递归的方式,我们能够高效地对链表进行分组反转。该方法不仅清晰易懂,而且具有较好的时间和空间复杂度表现。需要注意reverse反转区间[left,right)的节点,不包括right。

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

文章讲解

视频讲解

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

image.png

解题思路

本题首先使用快慢指针找到倒数n+1个节点,然后删除倒数第n个节点,注意需要用一个虚拟节点dummy来防止一个元素时候找倒数第2个元素失败。

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
class Solution:
    def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
        # base case sz=1不用管
        if head is None:
            return head

        # 删除
        dummy = ListNode(next = head)
        prev = self.findFromEnd(dummy, n+1) # 避免一个节点时候,找倒数第2个
        prev.next = prev.next.next

        return dummy.next


    def findFromEnd(self,head:ListNode, n:int)->ListNode:
        ''' 找到链表倒数第n个节点 '''
        # fast先走k步
        slow, fast = head,head
        for i in range(n):
            fast = fast.next

        # 同速前进,直到fast为空
        while fast != None:
            slow = slow.next
            fast = fast.next
        return slow

总结

该题的关键在于理解快慢指针的思路,并正确地使用指针来找到和删除链表的倒数第 N 个节点,快指针先走N步到空时,慢指针刚好是倒数第N个节点 。通过快慢指针的方法,我们可以高效地解决链表问题。

面试题 02.07. 链表相交

文章讲解

labuladong讲解

题目:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。图示两个链表在节点 c1 开始相交:

image.png

解题思路

通过让两个指针在两个链表之间来回移动,最终它们一定会在交点处相遇,或者指向null。

  • 1 初始化两个指针 p1 和 p2,分别指向链表 headA 和 headB 的头节点。
  • 2 让 p1 和 p2 同时前进,当 p1 到达链表 headA 的末尾时,将其移动到链表 headB 的头节点;当 p2 到达链表 headB 的末尾时,将其移动到链表 headA 的头节点。
  • 3 重复步骤2,直到 p1 和 p2 指向同一个节点。这个节点就是两个链表的交点。如果两个链表没有交点,那么 p1 和 p2 最终会同时指向 null,此时返回 null

image.png

image.png

  • 时间复杂度:O(n + m)
  • 空间复杂度:O(1)
class Solution:
    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        # base case

        p1, p2 = headA, headB
        while p1 != p2:
            p1 = headB if p1 is None else p1.next
            # p2 = headA if p2 is None else p2.next  # 不要复制
            p2 = headA if p2 is None else p2.next

        return p1

总结

解决此问题的关键是理解如何有效地遍历两个链表,通过交替移动指针,确保至少有一个指针始终跟随相交部分的路径。此外,注意在更新指针时避免错误地使它们指向同一节点。通过这种方式,我们可以在 O(n + m) 的时间内找到两个链表相交的第一个节点。

142. 环形链表 II

文章讲解

视频讲解

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

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

不允许修改 链表。

image.png

解题思路

快慢指针,slow走k步,fast走2k步,到一个相遇点后,把slow放回head,然后同速而行,再走k-m步就相遇(m为相交点到相遇点的距离)

image.png

  • 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
  • 空间复杂度: O(1)

#补充

class Solution:
    def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
        if head is None:
            return head

        slow, fast = head, head
        while fast.next != None:
            slow = slow.next
            fast = fast.next.next

            # 相遇,跳出循环
            if slow == fast:
                break
        
        # fast 遇到空指针说明没有环
        if fast is None or fast.next is None:
            return None

        # 重新指向头结点,快慢指针同步前进,相交点就是环起点
        slow = head
        while slow != fast:
            slow = slow.next
            fast = fast.next
        
        return slow

总结

这道题的难点主要在于理解快慢指针的原理,以及如何利用这个原理来找到环的入口节点。如果不理解这个原理,很难想到这种解法。另外,还需要仔细推导整个过程,确保每一步都是正确的。