LEETCODE 148Medium

排序链表

链表不能随机访问,堆排快排都施展不开,但归并只需要“找中点、断开、合并”这三个链表恰好擅长的动作。

问题拆解

对链表做升序排序,要求 O(n log n) 时间。把节点值倒进数组排完再写回去当然可行,但多了 O(n) 空间,也失去了链表题的意义。在 O(n log n) 的排序家族里挑一个:快排依赖随机访问选轴、堆排要下标计算,都和链表八字不合;归并排序不同——它的三个子动作“对半分、递归、合并有序段”在链表上一个比一个自然,合并甚至比数组还省(改指针即可,不用挪元素)。

对半分靠快慢指针:快指针一次两步、慢指针一次一步,快到尾时慢在中点,断开 slow.next 就得到两条独立的子链。

归并排序的数组版短板在合并要开辅助数组,链表版恰好没有这个负担——合并两条有序链表(21 题)是 O(1) 额外空间的。分治骨架 + 21 题的合并,就是完整解法。

快慢指针断链 + 递归归并

public ListNode sortList(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }
    ListNode slow = head, fast = head.next; // fast 领先一步,保证 slow 停在左半段末尾
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    ListNode mid = slow.next;
    slow.next = null; // 断链,两半各自独立
    return merge(sortList(head), sortList(mid));
}

private ListNode merge(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode();
    ListNode tail = dummy;
    while (l1 != null && l2 != null) {
        if (l1.val <= l2.val) {
            tail.next = l1;
            l1 = l1.next;
        } else {
            tail.next = l2;
            l2 = l2.next;
        }
        tail = tail.next;
    }
    tail.next = l1 != null ? l1 : l2;
    return dummy.next;
}
def sortList(head):
    if head is None or head.next is None:
        return head
    slow, fast = head, head.next  # fast 领先一步,保证 slow 停在左半段末尾
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    mid = slow.next
    slow.next = None  # 断链,两半各自独立
    return merge(sortList(head), sortList(mid))


def merge(l1, l2):
    dummy = ListNode()
    tail = dummy
    while l1 and l2:
        if l1.val <= l2.val:
            tail.next = l1
            l1 = l1.next
        else:
            tail.next = l2
            l2 = l2.next
        tail = tail.next
    tail.next = l1 if l1 else l2
    return dummy.next
func sortList(head *ListNode) *ListNode {
    if head == nil || head.Next == nil {
        return head
    }
    slow, fast := head, head.Next // fast 领先一步,保证 slow 停在左半段末尾
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
    }
    mid := slow.Next
    slow.Next = nil // 断链,两半各自独立
    return mergeLists(sortList(head), sortList(mid))
}

func mergeLists(l1, l2 *ListNode) *ListNode {
    dummy := &ListNode{}
    tail := dummy
    for l1 != nil && l2 != nil {
        if l1.Val <= l2.Val {
            tail.Next = l1
            l1 = l1.Next
        } else {
            tail.Next = l2
            l2 = l2.Next
        }
        tail = tail.Next
    }
    if l1 != nil {
        tail.Next = l1
    } else {
        tail.Next = l2
    }
    return dummy.Next
}
// Rust 里快慢指针要同时持有两个可变借用,改用先数长度再按 len/2 断链,
// take() 摘下右半段后两条链所有权独立,递归可以安全进行。
pub fn sort_list(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
    let mut len = 0;
    let mut p = head.as_ref();
    while let Some(node) = p {
        len += 1;
        p = node.next.as_ref();
    }
    Self::merge_sort(head, len)
}

fn merge_sort(mut head: Option<Box<ListNode>>, len: usize) -> Option<Box<ListNode>> {
    if len <= 1 {
        return head;
    }
    let mut cur = head.as_mut().unwrap();
    for _ in 0..len / 2 - 1 {
        cur = cur.next.as_mut().unwrap();
    }
    let right = cur.next.take(); // 断链,右半段所有权被拿走
    let left = Self::merge_sort(head, len / 2);
    let right = Self::merge_sort(right, len - len / 2);
    Self::merge(left, right)
}

fn merge(mut l1: Option<Box<ListNode>>, mut l2: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
    let mut dummy = Box::new(ListNode::new(0));
    let mut tail = &mut dummy;
    while l1.is_some() && l2.is_some() {
        let source = if l1.as_ref().unwrap().val <= l2.as_ref().unwrap().val {
            &mut l1
        } else {
            &mut l2
        };
        let mut node = source.take().unwrap();
        *source = node.next.take();
        tail.next = Some(node);
        tail = tail.next.as_mut().unwrap();
    }
    tail.next = l1.or(l2);
    dummy.next
}

最隐蔽的坑在快指针的起点:fast 必须从 head.next 出发而不是 head。考虑只有两个节点的链表,若 fasthead 起步,循环结束时 slow 停在第二个节点,断链后左半是整条链、右半为空,递归规模不减,直接栈溢出。让 fast 领先一步,slow 才会停在左半段的末尾,保证每次都是真正的对半分。

另一个细节是断链本身:忘了 slow.next = null(Rust 里是 take()),左半段递归时会一路走到原链表尾部,两半根本没有分开。合并部分与 21 题逐字相同,<= 保持稳定性。

复杂度

指标 复杂度 原因
时间 O(n log n) 递归 log n 层,每层找中点 + 合并共 O(n)
空间 O(log n) 递归栈深度;合并本身只改指针

可以迁移的模式

  • 数据结构决定排序算法:不能随机访问就想归并,合并有序段只依赖顺序遍历;
  • 快慢指针找中点时,用 fast = head.next 起步让分割偏向左小右大,规避两元素的死循环;
  • 分治前先“断干净”,让子问题在物理上独立,递归才不会互相越界。

“找中点、断开、递归、合并”四步是链表分治的完整模板,148 是它最纯粹的用例。