算法题 排序链表

LeetCode 148. 排序链表

问题描述

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

示例 1:

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

示例 2:

输入:head = [-1,5,3,4,0]
输出:[-1,0,3,4,5]

示例 3:

输入:head = []
输出:[]

进阶要求:在 O(n log n) 时间复杂度和常数级空间复杂度下完成排序。

算法思路(归并排序)

采用归并排序解决链表排序问题,分为三个关键步骤:

  1. 分割链表:使用快慢指针找到链表中间节点,将链表分成两半
  2. 递归排序:对分割后的两个子链表递归排序
  3. 合并有序链表:合并两个已排序的子链表

自顶向下归并排序

/**
 * 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;
        }
        
        // 使用快慢指针找到链表的中间节点
        // 慢指针每次走1步,快指针每次走2步
        ListNode slow = head;
        ListNode fast = head.next;  // 快指针从head.next开始,确保慢指针停在中间节点的前驱
        
        // 移动快慢指针直到快指针到达链表末尾
        while (fast != null && fast.next != null) {
            slow = slow.next;        // 慢指针移动1步
            fast = fast.next.next;    // 快指针移动2步
        }
        
        // 分割链表为两部分
        ListNode mid = slow.next;  // 后半部分链表的头节点
        slow.next = null;          // 断开链表(将前半部分的尾节点next置为null)
        
        // 递归排序左右两个子链表
        ListNode left = sortList(head);   // 排序前半部分
        ListNode right = sortList(mid);   // 排序后半部分
        
        // 合并两个有序链表
        return merge(left, right);
    }
    
    // 合并两个有序链表
    private ListNode merge(ListNode l1, ListNode l2) {
        // 创建哑节点(dummy node)作为新链表的起始点
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;  // 指向新链表的尾节点
        
        // 遍历两个链表,比较节点值,将较小值接入新链表
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                tail.next = l1;  // 将l1接入新链表
                l1 = l1.next;   // 移动l1指针
            } else {
                tail.next = l2;  // 将l2接入新链表
                l2 = l2.next;    // 移动l2指针
            }
            tail = tail.next;    // 移动新链表的尾指针
        }
        
        // 将剩余部分直接接入新链表末尾
        tail.next = (l1 != null) ? l1 : l2;
        
        return dummy.next;  // 返回新链表的头节点(跳过哑节点)
    }
}

自底向上归并排序 - 空间复杂度 O(1)

class Solution {
    public ListNode sortList(ListNode head) {
        // 边界条件处理
        if (head == null || head.next == null) {
            return head;
        }
        
        // 计算链表长度
        int length = 0;
        ListNode node = head;
        while (node != null) {
            length++;
            node = node.next;
        }
        
        // 创建哑节点指向链表头
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        
        // 步长从1开始,每次倍增(1, 2, 4...)
        for (int step = 1; step < length; step <<= 1) {
            ListNode prev = dummy;    // 记录已排序部分的尾节点
            ListNode curr = dummy.next; // 当前待合并段的起始位置
            
            while (curr != null) {
                // 获取第一段链表(长度为step)
                ListNode head1 = curr;
                // 移动step-1步,找到第一段尾部
                for (int i = 1; i < step && curr.next != null; i++) {
                    curr = curr.next;
                }
                
                // 获取第二段链表(长度为step)
                ListNode head2 = curr.next;
                curr.next = null;  // 断开第一段与第二段的连接
                curr = head2;      // 移动curr到第二段头部
                // 移动step-1步,找到第二段尾部
                for (int i = 1; i < step && curr != null && curr.next != null; i++) {
                    curr = curr.next;
                }
                
                // 保存后续未处理链表的头节点
                ListNode next = null;
                if (curr != null) {
                    next = curr.next;
                    curr.next = null;  // 断开第二段与后续链表的连接
                }
                
                // 合并第一段和第二段链表
                ListNode merged = merge(head1, head2);
                // 将合并后的链表接入已排序部分
                prev.next = merged;
                // 移动prev到新链表的末尾
                while (prev.next != null) {
                    prev = prev.next;
                }
                // 处理下一组链表
                curr = next;
            }
        }
        return dummy.next;
    }
    
    // 合并两个有序链表(同上)
    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(0);
        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;
    }
}

算法分析

  1. 时间复杂度

    • 两种方法均为 O(n log n),归并排序的标准时间复杂度
    • 每层归并操作需要 O(n) 时间,递归深度为 O(log n)
  2. 空间复杂度

    • 自顶向下:O(log n),递归调用栈空间
    • 自底向上:O(1),仅使用有限几个指针变量

算法过程(自顶向下)

以输入 [4,2,1,3] 为例:

  1. 分割阶段

    • 快慢指针:slow 从4开始,fast 从2开始
    • 第一轮:slow=4→2,fast=2→1→3(fast.next=null 结束)
    • 分割结果:左链表 [4,2],右链表 [1,3]
  2. 递归排序左链表

    • 分割 [4,2] → 左子链表 [4],右子链表 [2]
    • 合并 [4][2][2,4]
  3. 递归排序右链表

    • 分割 [1,3] → 左子链表 [1],右子链表 [3]
    • 合并 [1][3][1,3]
  4. 合并结果

    • 合并 [2,4][1,3]
      • 比较2和1 → 取1
      • 比较2和3 → 取2
      • 比较4和3 → 取3
      • 剩余4 → 最终结果 [1,2,3,4]

关键点

  1. 快慢指针分割链表

    • 快指针速度是慢指针2倍
    • 当快指针到达末尾时,慢指针正好在中间
    • 注意断开连接:slow.next = null
  2. 递归终止条件

    • 链表为空或只有一个节点时直接返回
  3. 合并有序链表技巧

    • 使用哑节点(dummy)简化头节点处理
    • 比较两个链表节点值,取较小者接入新链表
    • 最后将剩余链表直接接入

两种方法对比

特性自顶向下自底向上
空间复杂度O(log n)(递归栈)O(1)(迭代实现)
代码复杂度简单直观较复杂(需手动控制步长)
适用场景面试推荐(易理解)满足严格空间要求

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1: 普通乱序
    ListNode head1 = createList(new int[]{4,2,1,3});
    ListNode sorted1 = solution.sortList(head1);
    printList(sorted1); // 预期: [1,2,3,4]
    
    // 测试用例2: 包含负数
    ListNode head2 = createList(new int[]{-1,5,3,4,0});
    ListNode sorted2 = solution.sortList(head2);
    printList(sorted2); // 预期: [-1,0,3,4,5]
    
    // 测试用例3: 空链表
    ListNode head3 = createList(new int[]{});
    ListNode sorted3 = solution.sortList(head3);
    printList(sorted3); // 预期: []
    
    // 测试用例4: 单节点链表
    ListNode head4 = createList(new int[]{0});
    ListNode sorted4 = solution.sortList(head4);
    printList(sorted4); // 预期: [0]
}

// 创建链表
private static ListNode createList(int[] arr) {
    ListNode dummy = new ListNode(0);
    ListNode curr = dummy;
    for (int val : arr) {
        curr.next = new ListNode(val);
        curr = curr.next;
    }
    return dummy.next;
}

// 打印链表
private static void printList(ListNode head) {
    StringBuilder sb = new StringBuilder();
    while (head != null) {
        sb.append(head.val).append(head.next != null ? "->" : "");
        head = head.next;
    }
    System.out.println("[" + sb.toString() + "]");
}

常见问题

  1. 为什么用快慢指针找中点?

    • 链表不支持随机访问,快慢指针是唯一 O(1) 空间找中点的方法
    • 快指针速度是慢指针2倍,确保精确找到中点
  2. 如何处理链表节点为奇数的情况?

    • 自然处理:快指针结束时,慢指针正好在中点位置
    • 示例:[1,2,3] → 分割为 [1,2][3]
  3. 哑节点(dummy)的作用是什么?

    • 简化链表操作:无需特殊处理头节点
    • 始终通过 dummy.next 访问真实头节点
  4. 自底向上方法中步长的作用?

    • 控制归并的链表长度(从1开始,每次倍增)
    • 模拟递归过程:先两两合并,再四四合并等
  5. 哪种方法更优?

    • 面试优先用自顶向下(简洁易理解)
    • 实际工程若要求严格 O(1) 空间,用自底向上
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值