【力扣-148. 排序链表】Python笔记

0 阅读3分钟

链表排序的“分治”魔法:归并排序的优雅应用


📚 核心知识点:归并排序与链表特性

因此,归并排序(Merge Sort) 成为了链表排序的最优解,因为它天生契合链表的特性:

  1. 分(Divide) :利用快慢指针找到链表中点,将链表一分为二。
  2. 治(Conquer) :递归地对左右两部分进行排序。
  3. 合(Merge) :利用双指针将两个有序链表合并为一个。

为什么是 O(nlogn)O(n \log n)

  • 找中点需要遍历链表,耗时 O(n)O(n)
  • 递归深度为 logn\log n
  • 总时间复杂度:O(nlogn)O(n \log n),且不需要额外空间(除了递归栈),非常高效。

📝 题目解析:LeetCode 148. 排序链表

题目描述
给你链表的头结点 head,请将其按 升序 排列并返回 排序后的链表

示例
输入:head = [4,2,1,3]
输出:[1,2,3,4]


💡 解题思路:递归归并的三部曲

这道题的核心在于“切分” 和 “合并”。我们可以把它想象成整理一副乱序的扑克牌:先把牌分成两堆,每堆再分两堆……直到每堆只有一张牌(自然有序),然后把它们两两合并,最终得到整副有序的牌。

步骤拆解

  1. 终止条件:如果链表为空或只有一个节点,它已经是有序的了,直接返回。

  2. 寻找中点(切分) :使用快慢指针

    • 快指针 fast 每次走两步,慢指针 slow 每次走一步。
    • fast 走到尽头,slow 正好在中点。
    • 关键一步:我们需要切断 slow 前面的连接,将链表分为 [head...prev][slow...end] 两部分。
  3. 递归排序:分别对左右两段链表调用 sortList

  4. 合并链表:调用 merge 函数,将两个有序链表合并成一个。


💻 代码实现(Python3)

代码如下:

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

class Solution:
    def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        # 1. 递归终止条件:空节点 or 单个节点(已经有序)
        if not head or not head.next:
            return head

        # ===================== 步骤1:找中点并切断链表 =====================
        # 慢指针、快指针、前驱(用于切断)
        slow, fast, prev = head, head, None
        while fast and fast.next:
            prev = slow       # 保存慢指针的前一个节点
            slow = slow.next  # 慢指针走1步
            fast = fast.next.next # 快指针走2步

        # 此时:
        # prev 是左半段的尾
        # slow 是右半段的头
        prev.next = None # 关键:切断链表!否则左半段会连着右半段,导致死循环

        # ===================== 步骤2:递归排序左右两半 =====================
        left = self.sortList(head)  # 排序左半段
        right = self.sortList(slow) # 排序右半段

        # ===================== 步骤3:合并两个有序链表 =====================
        # 复用 21题 的合并逻辑
        return self.merge(left, right)

    # 辅助函数:合并两个有序链表(源自 LeetCode 21)
    def merge(self, l1: ListNode, l2: ListNode) -> ListNode:
        dummy = ListNode(0) # 虚拟头节点,简化操作
        cur = dummy

        # 双指针比较,谁小接谁
        while l1 and l2:
            if l1.val <= l2.val:
                cur.next = l1
                l1 = l1.next
            else:
                cur.next = l2
                l2 = l2.next
            cur = cur.next

        # 处理剩余节点
        cur.next = l1 if l1 else l2
        return dummy.next

🔍 深度解析:为什么要用 prev 切断链表?

在找中点的循环中,你可能会问: “为什么不只用 slowfast,还要多一个 prev?”

原因
当我们找到中点 slow 时,链表依然是连通的:... -> prev -> slow -> ...
如果不切断 prev.next,左半部分链表 head 的尾部依然指向右半部分。

  • 如果我们递归处理 head,它会包含整个右半部分,导致无限递归(栈溢出)
  • 所以,prev.next = None 这一步是灵魂操作,它确保了左半部分是一个独立的、被截断的链表。

📌 总结

这道题是链表操作的综合考察:

  1. 快慢指针找中点(分治的基础)。
  2. 递归思维(将大问题拆解为小问题)。
  3. 双指针合并(有序链表的经典操作)。

掌握了这道题,你对链表的理解绝对能上一个大台阶!加油,打工人!