链表排序的“分治”魔法:归并排序的优雅应用
📚 核心知识点:归并排序与链表特性
因此,归并排序(Merge Sort) 成为了链表排序的最优解,因为它天生契合链表的特性:
- 分(Divide) :利用快慢指针找到链表中点,将链表一分为二。
- 治(Conquer) :递归地对左右两部分进行排序。
- 合(Merge) :利用双指针将两个有序链表合并为一个。
为什么是 ?
- 找中点需要遍历链表,耗时 。
- 递归深度为 。
- 总时间复杂度:,且不需要额外空间(除了递归栈),非常高效。
📝 题目解析:LeetCode 148. 排序链表
题目描述:
给你链表的头结点 head,请将其按 升序 排列并返回 排序后的链表。
示例:
输入:head = [4,2,1,3]
输出:[1,2,3,4]
💡 解题思路:递归归并的三部曲
这道题的核心在于“切分” 和 “合并”。我们可以把它想象成整理一副乱序的扑克牌:先把牌分成两堆,每堆再分两堆……直到每堆只有一张牌(自然有序),然后把它们两两合并,最终得到整副有序的牌。
步骤拆解:
-
终止条件:如果链表为空或只有一个节点,它已经是有序的了,直接返回。
-
寻找中点(切分) :使用快慢指针。
- 快指针
fast每次走两步,慢指针slow每次走一步。 - 当
fast走到尽头,slow正好在中点。 - 关键一步:我们需要切断
slow前面的连接,将链表分为[head...prev]和[slow...end]两部分。
- 快指针
-
递归排序:分别对左右两段链表调用
sortList。 -
合并链表:调用
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 切断链表?
在找中点的循环中,你可能会问: “为什么不只用 slow 和 fast,还要多一个 prev?”
原因:
当我们找到中点 slow 时,链表依然是连通的:... -> prev -> slow -> ...。
如果不切断 prev.next,左半部分链表 head 的尾部依然指向右半部分。
- 如果我们递归处理
head,它会包含整个右半部分,导致无限递归(栈溢出) 。 - 所以,
prev.next = None这一步是灵魂操作,它确保了左半部分是一个独立的、被截断的链表。
📌 总结
这道题是链表操作的综合考察:
- 快慢指针找中点(分治的基础)。
- 递归思维(将大问题拆解为小问题)。
- 双指针合并(有序链表的经典操作)。
掌握了这道题,你对链表的理解绝对能上一个大台阶!加油,打工人!