LeetCode 148. 排序链表:归并排序的细节解析

文章目录

    • 题目描述
    • 一、方法思路:归并排序的核心步骤
    • 二、关键实现细节:快慢指针分割链表
      • 1. 快慢指针的初始化问题
      • 2. 为什么选择 `fast = head.next`?
        • 示例1:链表长度为偶数(`1->2->3->4`)
    • 三、完整代码实现
    • 四、复杂度分析
    • 五、总结

题目描述

LeetCode 148题要求对链表进行排序,时间复杂度需为 O(n log n),且空间复杂度为 O(log n)。由于链表的特殊结构(无法随机访问),归并排序成为实现这一目标的理想选择。本文将详细解析如何通过归并排序解决该问题,并重点探讨一个关键细节:为什么快指针的初始化方式会影响分割结果


一、方法思路:归并排序的核心步骤

归并排序的核心思想是“分治”:

  1. 分割链表:将链表分成两个尽可能均匀的子链表。
  2. 递归排序:对子链表递归排序。
  3. 合并有序链表:将两个有序子链表合并为一个有序链表。

其中,均匀分割链表是保证时间复杂度的关键。若分割不均,递归深度或每层操作次数可能增加,导致效率下降。


二、关键实现细节:快慢指针分割链表

1. 快慢指针的初始化问题

在分割链表时,我们需要找到中间节点。常见的实现方式是使用快慢指针

  • 慢指针 slow:每次移动一步。
  • 快指针 fast:每次移动两步。

但快指针的初始化方式直接影响分割结果。以下是两种初始化方式的对比:

初始化方式 fast = head.next fast = head
链表长度为偶数时 均匀分割(左半部分长度=右半部分) 右半部分可能过短
链表长度为奇数时 均匀分割(左半部分长度=右半部分-1) 均匀分割

2. 为什么选择 fast = head.next

通过示例分析不同初始化方式的分割结果:

示例1:链表长度为偶数(1->2->3->4
  • fast = head.next(初始值fast=2):
    第1步:slow=2, fast=4 → fast.next=null,停止循环。
    分割结果:左半部分`1->2`,右半部分`3->4`(均匀)。
    
  • fast = head(初始值fast=1):
    第1步:slow=2, fast=3 → fast.next存在,继续循环。
    第2步:slow=3, fast=null → 停止循环。
    分割结果:左半部分`1->2->3`,右半部分`4`(不均匀)。
    

结论fast = head.next 确保了链表被均匀分割,而 fast = head 在偶数长度时会导致右半部分过短。


三、完整代码实现

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;
        }
        // 使用快慢指针找到中间节点(关键:fast初始化为head.next)
        ListNode slow = head;
        ListNode fast = head.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode mid = slow.next;
        slow.next = null; // 分割链表为两部分
        // 递归排序左右子链表
        ListNode left = sortList(head);
        ListNode right = sortList(mid);
        // 合并有序链表
        return merge(left, right);
    }
    
    private ListNode merge(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);
        ListNode curr = dummy;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                curr.next = l1;
                l1 = l1.next;
            } else {
                curr.next = l2;
                l2 = l2.next;
            }
            curr = curr.next;
        }
        curr.next = (l1 != null) ? l1 : l2;
        return dummy.next;
    }
}

四、复杂度分析

  1. 时间复杂度O(n log n)
    • 每层递归需要 O(n) 时间合并链表,递归深度为 O(log n)
  2. 空间复杂度O(log n)
    • 递归调用栈的深度为 O(log n)

五、总结

  1. 均匀分割的重要性:确保递归深度为 O(log n),避免时间复杂度退化。
  2. 快慢指针的初始化细节fast = head.next 保证了链表被均匀分割,尤其是在链表长度为偶数时。
  3. 归并排序的优势:适合链表的顺序访问特性,合并操作无需额外空间(仅需修改指针)。

通过正确实现快慢指针的分割逻辑,归并排序能够高效解决链表的排序问题,满足题目对时间复杂度和空间复杂度的要求。

你可能感兴趣的:(2025,Top100,详解,leetcode,链表,算法)