148. 排序链表 (Medium)

专题归类: 04-链表 LeetCode 链接: https://leetcode.cn/problems/sort-list/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

给你链表的头结点 head,将其按升序排列并返回排序后的链表。

进阶: 你可以在 O(n log n) 时间复杂度和常数级空间复杂度下解决吗?

示例:

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

题目详细分析

  • 数据范围: 链表中节点数在 [0, 5*10^4] 范围内,节点值在 [-10^5, 10^5] 范围内
  • 输入特征: 单链表,无序,需要排序
  • 核心约束: O(n log n) 时间和 O(1) 空间(进阶要求)——排除了插入排序 O(n^2)、也排除了自顶向下归并的 O(log n) 递归栈空间
  • 隐藏条件: 数组上的 O(n log n) 排序算法(快排、归并、堆排)需要随机访问,链表不支持随机访问,所以只有「归并排序」适合链表
  • 边界条件: 空链表、只有一个节点(已经有序)、所有节点值相同

小白版直白理解

就像整理一条乱序的项链,你不能像整理数组那样直接交换位置,因为只能一颗一颗地前后找。

一个有效的方法是「先拆再合」:先把项链不断对半拆开,直到每段只有一颗珠子(这时每段都是「有序」的);然后再两两合并,合并时按大小排列。这个过程就像淘汰赛:先拆散到个人,再两两比试合并。


解题思路

思路一:自顶向下归并排序(推荐)

为什么这样想: 链表归并排序天然适合链表结构。找中点可以用快慢指针(O(n)),断开后递归排序,然后合并两个有序链表(复用 21 题)。整体 O(n log n),但递归栈空间为 O(log n)。

关键洞察: 快慢指针找中点时,fast 初始化为 head.next 可以使 slow 停在「中间偏左」,这样 slow.next 就是右半段的头,然后将 slow.next 断开。

def sortList(head):
    """自顶向下归并排序"""
    if not head or not head.next:
        return head
    
    # 1. 快慢指针找中点
    slow, fast = head, head.next
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    
    mid = slow.next   # 右半段起点
    slow.next = None  # 断开为左右两段
    
    # 2. 递归排序左右两半
    left = sortList(head)
    right = sortList(mid)
    
    # 3. 合并两个有序链表
    dummy = ListNode(0)
    cur = dummy
    while left and right:
        if left.val <= right.val:
            cur.next = left
            left = left.next
        else:
            cur.next = right
            right = right.next
        cur = cur.next
    cur.next = left if left else right
    return dummy.next

思路二:自底向上归并排序(进阶,O(1) 空间)

思路讲解: 从长度为 1 的子链表开始,不断两两合并,直到完整链表。每轮合并所有长度为 sub_len 的子链表,下一轮 sub_len 翻倍。通过迭代而非递归实现 O(1) 空间。实现更复杂,但满足进阶的常数空间要求。

关键洞察: 需要用循环模拟归并的多层合并过程。核心是每次将链表拆分为多个长度为 sub_len 的块,两两合并,然后 sub_len *= 2。

def sortList(head):
    """自底向上归并排序(O(1) 空间)"""
    if not head or not head.next:
        return head
    
    # 计算链表长度
    length = 0
    cur = head
    while cur:
        length += 1
        cur = cur.next
    
    dummy = ListNode(0, head)
    sub_len = 1
    while sub_len < length:
        prev = dummy
        cur = dummy.next
        
        while cur:
            # 取第一段 sub_len 个节点
            left = cur
            right = split(left, sub_len)
            cur = split(right, sub_len)  # 剩余部分的头
            
            # 合并 left 和 right
            merged = merge(left, right)
            prev.next = merged
            # prev 移到合并后的尾部
            while prev.next:
                prev = prev.next
        
        sub_len *= 2
    
    return dummy.next
 
 
def split(head, n):
    """从 head 开始切下 n 个节点,返回剩余部分的头"""
    while head and n > 1:
        head = head.next
        n -= 1
    if not head:
        return None
    rest = head.next
    head.next = None  # 断开
    return rest
 
 
def merge(left, right):
    """合并两个有序链表"""
    dummy = ListNode(0)
    cur = dummy
    while left and right:
        if left.val <= right.val:
            cur.next = left
            left = left.next
        else:
            cur.next = right
            right = right.next
        cur = cur.next
    cur.next = left if left else right
    return dummy.next

思路三:转换为数组排序(不满足进阶要求)

思路讲解: 将链表值全部存入数组,对数组排序,再按顺序更新链表节点的值。简单但不满足 O(1) 空间要求。空间 O(n)。

def sortList(head):
    """数组辅助法(空间 O(n))"""
    if not head:
        return None
    
    vals = []
    cur = head
    while cur:
        vals.append(cur.val)
        cur = cur.next
    
    vals.sort()
    
    cur = head
    for val in vals:
        cur.val = val
        cur = cur.next
    
    return head

易错点

  • 快慢指针找中点: fast 初始化为 head.next 让 slow 停在左半段的末尾,而不是中点。右半段起点是 slow.next
  • 断开链表: slow.next = None 必须执行,否则递归时会出现无限循环
  • 自顶向下的空间: 自顶向下递归的空间复杂度是 O(log n)(递归栈),不是 O(1)
  • 自底向上的拆分: split 函数要小心处理剩余长度不足 n 的情况
  • merge 后的 prev: 合并后 prev 需要移动到合并链表的尾部,不要忘了
  • 空链表和单节点: 排序前先检查,空或只有一个节点直接返回

框架提炼

链表归并排序模板(自顶向下):

def sort_list(head):
    if not head or not head.next:
        return head
    
    # 找中点
    slow, fast = head, head.next
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    mid = slow.next
    slow.next = None
    
    # 递归 + 合并
    left = sort_list(head)
    right = sort_list(mid)
    return merge(left, right)

链表归并排序模板(自底向上): 通过 sub_len 翻倍迭代,避免递归。虽然代码更复杂,但实现了 O(1) 空间。核心操作是 split(切分链表)和 merge(合并有序链表)的反复调用。


关联题目