LeetCode 148.排序链表(归并排序・自顶向下递归)

0 阅读3分钟

image.png

image.png

一、题目大意

给链表头结点head,将链表按升序排序,返回排序完成后的链表。 要求时间复杂度 (O(n\log n))。

链表不支持随机访问,快排实现麻烦,优先选用归并排序

二、核心思路

归并排序分为两步:

  1. 分(分割链表) 使用快慢指针找到链表中点,preSlow记录慢指针前一个节点,preSlow.next=null切断链表,拆成左右两条子链表。 递归继续分割子链表,直到子链表只有 0 或 1 个节点(天然有序,递归终止)。
  2. 合(合并有序链表) 复用 LeetCode21 题:合并两个有序链表,用 dummy 虚拟头结点 + tail 尾指针拼接节点。

时间复杂度:(O(n\log n)) 空间复杂度:(O(\log n)),递归调用栈开销

三、完整 Java 代码

/**
 * Definition for singly‑linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode sortList(ListNode head) {
    
        //递归终止:空链表 / 单节点链表本身有序
        if(head == null || head.next == null) {
            return head;
        }
        ListNode fast = head;
        ListNode slow = head;
        ListNode preSlow = null;
        
        //1.快慢指针找中点
        while(fast != null && fast.next != null) {
            preSlow = slow;
            slow = slow.next;
            fast = fast.next.next;
        }
        
        preSlow.next = null; //2.切断链表,拆分成左右两段

        ListNode left = sortList(head);
        ListNode right = sortList(slow);
        
        //合并两个有序链表
        return mergeTwoLists(left,right);
    }

    //21题:合并两个有序链表
    private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    
        ListNode dummy = new ListNode(-1);
        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; //尾指针向后移动,高频易错点
        }
        
        //拼接剩余未遍历完的链表
        if(l1 != null) {
            tail.next = l1;
        } else {
            tail.next = l2;
        }
        return dummy.next;
    }
}

四、实例演示(表格)

输入链表:4 → 2 → 1 → 3

递归层级当前处理链表preSlowslow切断后左链表切断后右链表操作
第 1 层(顶层)4→2→1→3节点 2节点 14→21→3递归处理左、右链表
第 2 层(处理左)4→2节点 4节点 24null递归处理左右
第 3 层(最底层左)4--4null到达递归出口直接返回 4
第 3 层(最底层右)2--2null到达递归出口直接返回 2
回溯第 2 层42合并:merge(4,2)2→4
第 2 层(处理右)1→3节点 1节点 31null递归处理左右
第 3 层(底层左)1--1null递归出口返回 1
第 3 层(底层右)3--3null递归出口返回 3
回溯第 2 层13合并:merge(1,3)1→3
回溯第 1 层(顶层)2→41→3合并:merge(2→4,1→3)1→2→3→4

执行流程:不断向下递归分割,分割到单个节点触发递归终止;之后一层层回溯,执行 merge 合并有序链表。

五、语法坑与边界条件

  1. 必须保存 preSlow,执行preSlow.next=null切断链表;不切断链表,递归不会分割,死循环。
  2. mergeTwoLists 的 while 条件:l1 != null && l2 != null,是&&不是||;只有两个链表都不为空,才能读取.val,避免空指针。
  3. tail = tail.next千万不能漏写:不移动尾指针,tail 一直钉死在 dummy,会反复覆盖节点,输出结果只有少量节点。
  4. 递归终止条件:head == null || head.next == null,空链表、单个节点直接返回。
  5. 递归版本空间(O(\log n));面试如果要求(O(1))空间,要写自底向上迭代归并。

六、个人感悟

  1. 链表归并排序核心分为两部分:快慢指针分割 + 合并有序链表(21 题) ,是旧题组合。
  2. 链表排序不适合快排,链表没有数组随机访问能力。
  3. merge 函数坑很多:循环条件、尾指针移动、剩余链表拼接,三处全部写对才能 AC。
  4. 递归虽然代码简洁,会占用栈空间;面试可以主动提自底向上迭代版本思路。