Leetcode 链表 2

题号直达: 21 合并两个有序链表 | 142 环形链表 II | 2 两数相加 | 19 删除链表的倒数第 N 个结点 | 24 两两交换链表中的节点

LeetCode 21. 合并两个有序链表 ↑

题目

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

提示

  • 两个链表的节点数目范围是 ([0, 50])
  • (-100 ≤ Node.val ≤ 100)
  • list1 和 list2 均按非递减顺序排列

示意图如下:

合并两个有序链表示意图

代码(Python)解法一:双指针原地拼接

class Solution(object):
    def mergeTwoLists(self, list1, list2):
        """
        :type list1: Optional[ListNode]
        :type list2: Optional[ListNode]
        :rtype: Optional[ListNode]
        """
        if not list1:
            return list2

        if not list2:
            return list1

        p1 = list1
        p2 = list2

        if list1.val < list2.val:
            p = list1
            p1 = p1.next
        else:
            p = list2
            p2 = p2.next

        while p1 is not None and p2 is not None:
            if p1.val < p2.val:
                p.next = p1
                p = p.next
                p1 = p1.next
            else:
                p.next = p2
                p = p.next
                p2 = p2.next

        if not p1:
            while p2 is not None:
                p.next = p2
                p = p.next
                p2 = p2.next
        else:
            while p1 is not None:
                p.next = p1
                p = p.next
                p1 = p1.next

        if list1.val < list2.val:
            return list1
        else:
            return list2

思路解析(解法一)

  • 双指针归并:维护两个指针 p1、p2 分别指向 list1、list2 当前尚未合并的节点,每次从二者中选取较小值接到结果链表的尾部,并向前推进对应指针,类似归并排序中的「归并」步骤。
  • 选择头结点:先比较 list1.val 和 list2.val,较小者作为最终结果链表的头结点 p,同时把对应的 p1 / p2 向前移动一位,后续在同一条链上继续追加。
  • 处理剩余部分:当其中一条链表先走到末尾后,只需把另一条链表剩下的部分整体接到结果尾部即可(代码中通过两个 while 补齐)。
  • 返回值注意点:最后返回的头结点必须是最开始选出来的那个较小值结点,因此根据初始比较结果在末尾返回 list1 或 list2。
  • 复杂度:每个节点只被访问一次并接到结果链表上,时间复杂度 (O(m + n)),其中 (m, n) 为两条链表长度;空间复杂度 (O(1)),仅使用了常数个指针,在原有链表节点上「原地重连」。

代码(Python)解法二:递归归并(写法更简洁)

class Solution(object):
    def mergeTwoLists(self, list1, list2):
        """
        :type list1: Optional[ListNode]
        :type list2: Optional[ListNode]
        :rtype: Optional[ListNode]
        """
        if not list1:
            return list2
        elif not list2:
            return list1
        elif list1.val < list2.val:
            list1.next = self.mergeTwoLists(list1.next, list2)
            return list1
        else:
            list2.next = self.mergeTwoLists(list1, list2.next)
            return list2

思路解析(解法二)

  • 递归含义:mergeTwoLists(list1, list2) 的语义是「将 list1 和 list2 合并成一个有序链表并返回头结点」。在每一层递归中,从当前两个头结点中选出较小者作为本层的头结点,并递归地合并剩余部分。
  • 状态转移:
    • 若 list1 为空,直接返回 list2;若 list2 为空,直接返回 list1;
    • 若 list1.val < list2.val,则 list1 应该排在前面,其余部分变为合并 list1.next 和 list2 的结果,即 list1.next = mergeTwoLists(list1.next, list2);
    • 否则对称地处理 list2。
  • 递归终止:当任一链表被耗尽(为空)时,返回另一条链表的剩余部分作为尾巴。
  • 复杂度:每个节点仍然只会被访问一次并连接到结果链表上,时间复杂度 (O(m + n)),空间复杂度由于递归栈为 (O(m + n))。相比迭代写法更短、更接近题意,但在 Python 中要注意递归深度上限。

LeetCode 142. 环形链表 II ↑

题目

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

评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。pos = -1 表示链表无环。注意:pos 仅用于标识环的实际情况,不作为函数参数传入。

提示

  • 链表中节点数目在范围 ([0, 10^4]) 内
  • (-10^5 ≤ Node.val ≤ 10^5)
  • pos 的值为 -1 或者链表中的一个有效索引

进阶:你能否使用 (O(1))(即常量)内存解决此问题?

代码(Python)解法一:用列表记录访问过的节点,第一次重复即为入环点

class Solution(object):
    def detectCycle(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        if not head or not head.next:
            return None

        p = head
        record = []
        while p is not None:
            if p in record:
                return p
            record.append(p)
            p = p.next
            
        return None

思路解析(解法一)

  • 整体思路:从头指针 head 出发顺着 next 一直向后遍历,同时维护一个容器 record 保存已经访问过的节点指针。遍历过程中:
    • 如果当前节点 p 已经出现在 record 中,说明我们第二次到达了同一个节点,它正是环的入口(因为从入口开始再次回到自身);立即返回这个节点;
    • 否则就把 p 加入 record,继续向后走。
  • 无环情况:若最终遍历到 p is None,说明尾指针指向 None,链表中不存在环,返回 None。
  • 与 141 题的区别:141 题只需要回答「有无环」,在第一次检测到重复节点时即可返回 True;本题则需要返回入环的节点本身,因此在检测到重复时返回 p。
  • 复杂度与不足:
    • 时间复杂度:在当前实现中,record 使用列表存储,p in record 为线性查找,最坏情况下时间复杂度接近 (O(n^2));
    • 空间复杂度:需要 (O(n)) 额外空间保存访问过的节点指针;
    • 若将 record 改为 set(),可以把查找均摊降到 (O(1)),整体时间复杂度为 (O(n)),但仍然没有达到进阶要求的 (O(1)) 额外空间;要实现常数空间,通常需要在快慢指针判环的基础上,再做一次指针“对齐”来精确定位环的入口。

代码(Python)解法二:快慢指针 + 数学推导定位入环点

class Solution(object):
    def detectCycle(self, head):
        """
        :type head: ListNode
        :rtype: ListNode
        """
        if not head or not head.next:
            return None

        slow = head          # 乌龟节点
        fast = head.next     # 兔子节点
        while fast is not None and fast.next is not None:
            fast = fast.next.next
            slow = slow.next
            if fast == slow:
                break

        if fast is None or fast.next is None:
            return None
            
        slow = slow.next

        ptr = head
        while ptr is not None:
            if ptr == slow:
                return slow
            ptr = ptr.next
            slow = slow.next

思路解析(解法二)

  • 先用快慢指针判断是否有环:和 141 题类似,用 slow 每次走一步、fast 每次走两步。如果最终 fast 或 fast.next 先走到 None,说明无环,直接返回 None;否则在某个时刻会有 fast == slow,说明指针相遇于环内某一点。
  • 为什么能找到入环点:设链表起点到入环点距离为 (a),入环点到相遇点距离为 (b),环长度为 (c)。可推出当快慢指针第一次相遇时:
    • 慢指针走了 (a + b);
    • 快指针走了 (a + b + k \cdot c)(多绕了若干圈);
    • 又因为快指针速度是慢指针 2 倍,有 (2(a + b) = a + b + k \cdot c),从而得到 (a = k \cdot c - b)。
      这说明从相遇点再走 (k \cdot c - b) 步就能回到入环点;而从链表头节点走 (a) 步也正好到入环点。
  • 指针对齐定位入口:在代码中,让 slow 从相遇点(这里先补走一步到真正的相遇位置)继续每次走一步,同时新指针 ptr 从 head 出发,每次走一步。根据上面的推导,当 ptr == slow 时,两者恰好在入环点相遇,即为所求。
  • 复杂度与优势:时间复杂度 (O(n)),空间复杂度 (O(1)),只用了常数个指针变量,是满足进阶要求的标准做法;相比解法一不需要额外容器存储访问记录。
环形链表 II 快慢指针定位入口示意图

LeetCode 2. 两数相加 ↑

题目

给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例

  • 输入:l1 = [9, 9, 9, 9, 9, 9, 9],l2 = [9, 9, 9, 9]
  • 输出:[8, 9, 9, 9, 0, 0, 0, 1](表示 9,999,999 + 9,999 = 10,009,998)

提示

  • 每个链表中的节点数在范围 ([1, 100]) 内
  • (0 ≤ Node.val ≤ 9)
  • 题目数据保证列表表示的数字不含前导零(除了数字 0 本身)

代码(Python)解法一:分阶段模拟

# 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 addTwoNumbers(self, l1, l2):
        """
        :type l1: Optional[ListNode]
        :type l2: Optional[ListNode]
        :rtype: Optional[ListNode]
        """
        p = ListNode(0)
        head = p
        record = 0
        while l1 and l2:
            p.val = (l1.val + l2.val + record) % 10
            record = (l1.val + l2.val + record) // 10
            l1 = l1.next
            l2 = l2.next
            if l1 or l2:
                p.next = ListNode(0)
                p = p.next

        if not l1:
            while l2 != None:
                p.val = (record + l2.val) % 10
                record = (record + l2.val) // 10
                if l2.next:
                    p.next = ListNode(0)
                    p = p.next  
                l2 = l2.next

        else:
            while l1 != None:
                p.val = (record + l1.val) % 10
                record = (record + l1.val) // 10
                if l1.next:
                    p.next = ListNode(0)
                    p = p.next
                l1 = l1.next

        if record != 0:
            p.next = ListNode(record)

        return head

思路解析(解法一)

  • 整体思路:模拟竖式加法,同时遍历两条链表,逐位相加并处理进位。用 record 记录进位(0 或 1),每次 (l1.val + l2.val + record) % 10 得到当前位,// 10 得到新的进位。
  • 分阶段处理:先处理两条链表都未走完的部分(while l1 and l2),再根据哪条链表更长,单独处理剩余部分;最后若仍有进位(record != 0),需要新建一个节点。
  • 创建新链表:使用虚拟头 head 和指针 p 逐步构建结果链表,每次需要继续时创建 ListNode(0) 并移动 p。
  • 复杂度:每个节点最多访问一次,时间复杂度 (O(\max(m, n))),空间复杂度 (O(\max(m, n))) 用于存储结果链表。

代码(Python)解法二:哑节点统一循环(更简洁)

# 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 addTwoNumbers(self, l1, l2):
        """
        :type l1: Optional[ListNode]
        :type l2: Optional[ListNode]
        :rtype: Optional[ListNode]
        """
        dummy = ListNode(0)  # 创建哑节点
        current = dummy
        carry = 0  # 进位
        
        while l1 or l2 or carry:
            # 获取当前位的值
            val1 = l1.val if l1 else 0
            val2 = l2.val if l2 else 0
            
            # 计算当前位的和及进位
            total = val1 + val2 + carry
            carry = total // 10
            current.next = ListNode(total % 10)
            
            # 移动指针
            current = current.next
            if l1:
                l1 = l1.next
            if l2:
                l2 = l2.next
        
        return dummy.next  # 返回哑节点的下一个节点(真正的头节点)

思路解析(解法二)

  • 本质相同:与解法一相同,均为模拟竖式加法的思路,逐位相加并处理进位。
  • 哑节点 + 统一循环:使用哑节点 dummy 后,只需一个 while l1 or l2 or carry 即可覆盖所有情况——两条链表都未走完、仅剩一条、以及最后进位,无需分阶段写三个分支。
  • 写法更简洁:用 val1 = l1.val if l1 else 0 处理空指针,逻辑更集中,代码更短。

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

题目

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

提示

  • 链表中结点的数目为 (sz)
  • (1 ≤ sz ≤ 30)
  • (0 ≤ Node.val ≤ 100)
  • (1 ≤ n ≤ sz)

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

代码(Python)解法一:双指针一趟扫描

# 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 removeNthFromEnd(self, head, n):
        """
        :type head: Optional[ListNode]
        :type n: int
        :rtype: Optional[ListNode]
        """
        front = head
        rear = head
        for i in range(n):
            front = front.next

        if front == None:
            return head.next

        front = front.next

        while front != None:
            front = front.next
            rear = rear.next

        rear.next = rear.next.next
        return head

思路解析(解法一)

  • 双指针思路:用 front 和 rear 两个指针,让 front 比 rear 超前 (n+1) 个节点。当 front 走到链表末尾(None)时,rear 恰好在待删除节点的前一个,执行 rear.next = rear.next.next 即可删除。
  • 先让 front 超前 n 步:for i in range(n) 后,front 指向第 (n) 个节点(从 1 计数);再执行一次 front = front.next,使 front 超前 rear 共 (n+1) 步。
  • 删除头结点:若 front 在第一次循环后即为 None,说明链表长度为 (n),待删除的是头结点,直接返回 head.next。
  • 一趟扫描:满足进阶要求,时间复杂度 (O(sz)),空间复杂度 (O(1))。

代码(Python)解法二:递归(利用回溯得到倒序位置)

# 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 removeNthFromEnd(self, head, n):
        """
        :type head: Optional[ListNode]
        :type n: int
        :rtype: Optional[ListNode]
        """
        dummy = ListNode(0)  # Java中的无参构造对应Python的默认值
        dummy.next = head
        self._remove(dummy, n)
        return dummy.next
    
    def _remove(self, head, n):
        """
        :type head: ListNode
        :type n: int
        :rtype: int
        """
        if head.next is None:
            return 1
        
        num = self._remove(head.next, n)
        if num == n:
            head.next = head.next.next
        return num + 1

思路解析(解法二)

  • 递归核心:_remove(head, n) 的返回值表示「从当前 head 到链表末尾共有多少个节点」。递归先一路到底(head.next is None 时返回 1),回溯时 num 从尾到头递增,相当于从后往前数。
  • 何时删除:当回溯到某一层时 num == n,说明当前 head.next 正是倒数第 (n) 个节点,执行 head.next = head.next.next 即可删除。
  • 哑节点:在 head 前加哑节点 dummy,可统一处理删除头结点的情况,最后返回 dummy.next。
  • 规律总结:链表倒序相关的问题(如倒数第 k 个、从尾到头遍历等)总可以采用递归方法,利用递归回溯时「从尾到头」的顺序自然得到倒序信息。
  • 复杂度:时间复杂度 (O(sz)),空间复杂度 (O(sz))(递归栈深度)。

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

题目

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

提示

  • 链表中节点的数目在范围 ([0, 100]) 内
  • (0 ≤ Node.val ≤ 100)

代码(Python)解法一:递归

class Solution(object):
    def swapPairs(self, head):
        """
        :type head: Optional[ListNode]
        :rtype: Optional[ListNode]
        """
        if head == None or head.next == None:
            return head

        newHead = head.next
        head.next = self.swapPairs(newHead.next)
        newHead.next = head
        return newHead

思路解析(解法一)

  • 递归含义:swapPairs(head) 表示「对以 head 为头的链表进行两两交换,并返回交换后的新头结点」。
  • 终止条件:若链表为空或只有一个节点,无需交换,直接返回 head。
  • 交换逻辑:设当前两个节点为 head 和 newHead = head.next。先递归处理 newHead.next 之后的子链表,得到交换后的子链表头;再将 head 接到子链表后面,newHead 接到 head 前面,即完成本对交换;最后返回 newHead 作为本段的新头。
  • 复杂度:每个节点访问一次,时间复杂度 (O(n)),空间复杂度 (O(n))(递归栈)。

代码(Python)解法二:迭代(哑节点 + 指针调整)

class Solution(object):
    def swapPairs(self, head):
        """
        :type head: Optional[ListNode]
        :rtype: Optional[ListNode]
        """
        dummyHead = ListNode(0)
        dummyHead.next = head
        temp = dummyHead
        while temp.next != None and temp.next.next != None:
            node1 = temp.next
            node2 = temp.next.next
            temp.next = node2
            node1.next = node2.next
            node2.next = node1
            temp = node1

        ans = dummyHead.next
        del dummyHead
        return ans

思路解析(解法二)

  • 哑节点:在 head 前加 dummyHead,使交换第一对时逻辑与后续对一致,无需单独处理头结点。
  • 迭代逻辑:temp 指向「当前待交换 pair 的前驱」。每次循环中,node1、node2 为待交换的两个节点;执行 temp.next = node2、node1.next = node2.next、node2.next = node1 完成交换;再将 temp 移到 node1(交换后 pair 的尾结点),作为下一对的前驱。
  • 终止条件:当 temp.next 或 temp.next.next 为 None 时,不足一对,退出循环。
  • 复杂度:时间复杂度 (O(n)),空间复杂度 (O(1)),满足进阶要求。

posted @ 2026-10-11 17:44  Phier  阅读(3)  评论(0)    收藏  举报