【LeetCode Hot 100 刷题日记(33/100)】148. 排序链表 —— 链表、归并排序、分治、双指针🧠

8 阅读8分钟

📌 题目链接:148. 排序链表 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:链表、归并排序、分治、双指针

⏱️ 目标时间复杂度:O(n log n)

💾 空间复杂度:O(log n)(自顶向下)或 O(1)(自底向上)

核心算法:归并排序(Merge Sort)
🔄 关键技巧:分治思想 + 快慢指针找中点 + 合并两个有序链表
💡 面试重点:如何在链表上实现 O(n log n) 时间复杂度且空间优化到常数级别?
🔥 典型考点:链表操作、递归与迭代对比、原地合并、边界处理


📊 题目分析

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

🎯 输入输出示例:

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

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

  • 输入head = []
    输出[]

❗️ 进阶要求:

是否能在 O(n log n) 时间复杂度和 O(1) 空间复杂度下完成?

👉 这是本题的核心挑战!普通排序算法如插入排序(O(n²))不满足;堆排序虽可达到 O(n log n),但难以在链表上高效实现;而归并排序正是最适合链表结构的高级排序方式。


🔍 核心算法及代码讲解

🌟 归并排序(Merge Sort)—— 分治法的经典应用

归并排序是一种典型的「分而治之」(Divide and Conquer)算法,其基本思想如下:

1. Divide: 将问题分解为更小的子问题
2. Conquer: 递归解决子问题
3. Combine: 将子问题的解合并成原问题的解

对于链表来说,由于没有随机访问能力(无法像数组那样用索引),我们不能直接使用下标划分,而是通过 快慢指针(Fast-Slow Pointer) 找到链表中点来拆分。

✅ 为什么选择归并排序?

排序算法时间复杂度空间复杂度是否适合链表
插入排序O(n²)O(1)✅ 可行但慢
快速排序平均 O(n log n),最坏 O(n²)O(log n)❌ 不稳定,容易栈溢出
堆排序O(n log n)O(1)❌ 操作复杂,不适合
归并排序O(n log n)O(log n) 或 O(1)最优选择

✅ 特别说明:归并排序在链表上的优势在于:

  • 不需要额外数组存储元素
  • 合并过程天然支持链式结构
  • 可以通过“自底向上”方式避免递归栈空间消耗

🧩 解题思路(分步详解)

我们将采用两种方法来解决问题:

✅ 方法一:自顶向下归并排序(递归版)

👉 使用递归实现归并排序,逻辑清晰易懂,但空间复杂度为 O(log n)

步骤分解:

  1. 终止条件判断

    • 若链表为空或只有一个节点 → 直接返回
    • 即:if (head == nullptr || head->next == tail) → 返回 head
  2. 找到中点

    • 使用 快慢指针 技术定位中点
    • 快指针每次走两步,慢指针走一步
    • 当快指针到达末尾时,慢指针指向中点
  3. 递归拆分

    • 以中点为界,将链表分为左右两部分
    • 分别对左右部分进行排序
  4. 合并有序链表

    • 使用「21. 合并两个有序链表」的思路合并结果
  5. 返回最终结果

📌 关键点:必须断开中点后继连接,防止环路!


✅ 方法二:自底向上归并排序(迭代版)

👉 通过循环控制子链表长度,逐步合并,空间复杂度可达 O(1)

步骤分解:

  1. 计算链表总长度 length
  2. 设置子链表长度 subLength = 1
  3. 循环合并
    • 每次将链表拆成若干个长度为 subLength 的子链表
    • 两两合并成长度为 2 * subLength 的有序链表
    • 更新 subLength *= 2
  4. 重复直到 subLength >= length

🌟 优点:无需递归,空间复杂度为 O(1) ⚠️ 缺点:代码稍复杂,边界处理需谨慎


📈 算法分析

方法时间复杂度空间复杂度是否满足进阶要求
自顶向下O(n log n)O(log n)❌ 不完全满足(非 O(1))
自底向上O(n log n)O(1)✅ 完全满足

🔍 为什么自底向上能省空间? 因为它用的是 迭代代替递归,不再依赖系统调用栈,仅用几个指针变量即可完成所有操作。


💻 代码实现(完整模板 + 行注释)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

class Solution {
public:
    // 主函数:入口
    ListNode* sortList(ListNode* head) {
        return sortList(head, nullptr);  // 传入尾部哨兵
    }

private:
    // 递归版本:自顶向下归并排序
    ListNode* sortList(ListNode* head, ListNode* tail) {
        if (head == nullptr) {  // 边界:空链表
            return head;
        }
        if (head->next == tail) {  // 只有一个节点
            head->next = nullptr;  // 断开连接,防止环
            return head;
        }

        // 使用快慢指针找中点
        ListNode* slow = head;
        ListNode* fast = head;
        while (fast != tail) {
            slow = slow->next;          // 慢指针走一步
            fast = fast->next;          // 快指针走两步
            if (fast != tail) {         // 避免越界
                fast = fast->next;
            }
        }
        ListNode* mid = slow;           // 中点位置

        // 递归排序左右两部分
        ListNode* left = sortList(head, mid);
        ListNode* right = sortList(mid, tail);

        // 合并两个已排序的链表
        return merge(left, right);
    }

    // 合并两个有序链表(经典题21)
    ListNode* merge(ListNode* head1, ListNode* head2) {
        ListNode* dummyHead = new ListNode(0);  // 虚拟头节点
        ListNode* temp = dummyHead;             // 当前指针
        ListNode* temp1 = head1;                // 左链表指针
        ListNode* temp2 = head2;                // 右链表指针

        // 双指针合并
        while (temp1 != nullptr && temp2 != nullptr) {
            if (temp1->val <= temp2->val) {
                temp->next = temp1;  // 选较小值
                temp1 = temp1->next;
            } else {
                temp->next = temp2;
                temp2 = temp2->next;
            }
            temp = temp->next;  // 移动当前指针
        }

        // 处理剩余节点
        if (temp1 != nullptr) {
            temp->next = temp1;
        } else if (temp2 != nullptr) {
            temp->next = temp2;
        }

        return dummyHead->next;  // 返回真实头节点
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    // 示例测试用例
    ListNode* head = new ListNode(4);
    head->next = new ListNode(2);
    head->next->next = new ListNode(1);
    head->next->next->next = new ListNode(3);

    Solution sol;
    ListNode* result = sol.sortList(head);

    // 输出结果
    while (result != nullptr) {
        cout << result->val << " ";
        result = result->next;
    }
    cout << endl;

    return 0;
}

🔄 自底向上版本(补充,供面试拓展)

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if (head == nullptr) return head;

        // 第一步:求链表长度
        int length = 0;
        ListNode* node = head;
        while (node != nullptr) {
            length++;
            node = node->next;
        }

        // 创建虚拟头节点,便于操作
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;

        // 第二步:从长度为1开始,逐步合并
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            ListNode* prev = dummyHead;  // 上一个合并段的尾部
            ListNode* curr = dummyHead->next;  // 当前遍历的节点

            while (curr != nullptr) {
                // 第一个子链表
                ListNode* head1 = curr;
                for (int i = 1; i < subLength && curr->next != nullptr; i++) {
                    curr = curr->next;
                }

                // 第二个子链表
                ListNode* head2 = curr->next;
                curr->next = nullptr;  // 断开第一个子链表
                curr = head2;

                // 构造第二个子链表
                for (int i = 1; i < subLength && curr != nullptr && curr->next != nullptr; i++) {
                    curr = curr->next;
                }

                // 下一段的起始点
                ListNode* next = nullptr;
                if (curr != nullptr) {
                    next = curr->next;
                    curr->next = nullptr;
                }

                // 合并两个有序链表
                ListNode* merged = merge(head1, head2);
                prev->next = merged;

                // 找到合并后链表的最后一个节点
                while (prev->next != nullptr) {
                    prev = prev->next;
                }
                curr = next;
            }
        }

        return dummyHead->next;
    }

private:
    ListNode* merge(ListNode* head1, ListNode* head2) {
        ListNode* dummyHead = new ListNode(0);
        ListNode* temp = dummyHead;
        ListNode* temp1 = head1;
        ListNode* temp2 = head2;

        while (temp1 != nullptr && temp2 != nullptr) {
            if (temp1->val <= temp2->val) {
                temp->next = temp1;
                temp1 = temp1->next;
            } else {
                temp->next = temp2;
                temp2 = temp2->next;
            }
            temp = temp->next;
        }

        if (temp1 != nullptr) {
            temp->next = temp1;
        } else if (temp2 != nullptr) {
            temp->next = temp2;
        }

        return dummyHead->next;
    }
};

🧠 面试加分点总结

考点说明
🧩 快慢指针找中点是链表中点查找的标准解法,必须掌握
🔁 递归 vs 迭代递归简洁但占用栈空间;迭代复杂但空间最优
🧱 合并链表「21. 合并两个有序链表」是基础,必须熟练
🧼 边界处理head == nullptr, head->next == tail
🧠 分治思想是许多高级算法的核心,如快速排序、归并排序、线段树等
🧪 时空复杂度分析面试必问!要能说出每种方法的时间和空间代价

💡 建议:在面试中优先写出递归版本,再提出迭代优化方案,展示你对算法的理解深度。


🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪


📣 下一期预告:LeetCode 热题 100 第33题 —— 46.合并 K 个升序链表(困难)

🔹 题目:给你 k 个非空链表,每个链表都已按升序排列,请将所有链表合并为一个升序链表。

🔹 核心思路:使用最小堆(优先队列)维护每个链表的头节点,每次取出最小值并加入结果链表。

🔹 考点:堆(优先队列)、链表操作、多路归并、分治思想。

🔹 难度:困难,但却是大厂常见题型,尤其在设计“流式数据合并”场景中非常实用!

💡 提示:不要暴力合并,那样会超时!优先考虑堆或分治策略!

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!


🎯 坚持每天一题,算法不再是梦!