跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

归并排序时间复杂度 O(nlogn) 解析:LeetCode 148 排序链表

综述由AI生成通过 LeetCode 148 排序链表题目,深入解析归并排序时间复杂度为何为 O(nlogn)。文章从分治策略入手,分析递归树高度为 log n,每层处理节点总数为 n,从而得出总复杂度。对比了递归实现(空间 O(log n))与迭代实现(空间 O(1)),并与其他排序算法进行了对比。最后提供了完整的 Java 代码示例,包括寻找中点、合并链表及自顶向下和自底向上的两种解法。

人间过客发布于 2026/3/28更新于 2026/5/2324 浏览
归并排序时间复杂度 O(nlogn) 解析:LeetCode 148 排序链表

归并排序时间复杂度 O(nlogn) 解析:LeetCode 148 排序链表

一、引言

在刷 LeetCode 148.排序链表时,很多同学会对归并排序的时间复杂度 O(nlogn) 感到困惑:为什么它一定能达到这个复杂度?分解和合并的过程具体是如何贡献这个复杂度的?本文将通过详细的图解和代码分析,揭开归并排序时间复杂度背后的数学原理。

二、问题背景:LeetCode 148.排序链表

题目要求对链表进行排序,进阶要求时间复杂度 O(nlogn) 且常数级空间复杂度。归并排序正是满足这一要求的经典解法。

示例:

  • 输入:4 → 2 → 1 → 3
  • 输出:1 → 2 → 3 → 4

三、归并排序的核心思想:分而治之

归并排序采用分治策略,将排序问题分解为三个步骤:

  1. 分解:将原问题分解成若干个规模较小的子问题
  2. 解决:递归地解决这些子问题
  3. 合并:将子问题的解合并成原问题的解

其时间复杂度可以用递归公式表示:

T(n) = 2T(n/2) + O(n)

其中:

  • 2T(n/2):分解成两个规模为 n/2 的子问题所需时间
  • O(n):合并两个有序子序列所需时间

四、为什么是 O(nlogn)?从二叉树的角度理解

4.1 分解过程形成一棵"递归树"

以 8 个节点的链表为例:8 → 4 → 2 → 1 → 7 → 5 → 3 → 6

分解过程(递归地寻找中点):

(此处省略图片链接)

关键观察:

  • 树的高度 = log₂n(8 个节点需要 log₂8=3 层分解才能得到单个节点)
  • 每层处理的节点总数始终为 n(只是被分成了更小的子链表)
4.2 合并过程自底向上构建有序链表

从最底层的单个节点开始,逐层合并:

第 4 层(合并后):[8] [4] → [4,8] [2] [1] → [1,2] [7] [5] → [5,7] [3] [6] → [3,6]
第 3 层(合并后):[1,2,4,8] [3,5,6,7]
第 2 层(合并后):[1,2,3,4,5,6,7,8]

五、时间复杂度逐层剖析

5.1 分解阶段的工作量

在递归版本的代码中,分解阶段主要工作在 searchMid 函数:

public ListNode searchMid(ListNode head) {
    ListNode fast = head.next, slow = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow; // 返回中点
}

每层的工作量计算:

层数子链表个数每个子链表长度每层总遍历次数
第 1 层1nn/2
第 2 层2n/22 × (n/4) = n/2
第 3 层4n/44 × (n/8) = n/2
…………
第 log n 层n/22(n/2) × 1 ≈ n/2

结论:分解阶段每层的工作量都是 O(n),共有 log n 层,所以分解阶段总时间复杂度 = O(n) × O(log n) = O(n log n)

5.2 合并阶段的工作量

合并阶段的核心是 mergeList 函数:

public ListNode mergeList(ListNode head1, ListNode head2) {
    ListNode h = new ListNode();
    ListNode p = h;
    ListNode p1 = head1, p2 = head2;
    while (p1 != null && p2 != null) {
        if (p1.val <= p2.val) {
            p.next = p1;
            p1 = p1.next;
        } else {
            p.next = p2;
            p2 = p2.next;
        }
        p = p.next;
    }
    p.next = (p1 == null ? p2 : p1);
    return h.next;
}

每层的工作量计算:

层数合并的对数每对合并的比较次数每层总操作次数
第 1 层(自底向上)4 对(n/2 对)每对最多 2 次≤ 4×2 = 8 ≈ n
第 2 层2 对(n/4 对)每对最多 4 次≤ 2×4 = 8 ≈ n
第 3 层1 对(n/8 对)每对最多 8 次≤ 1×8 = 8 ≈ n

结论:合并阶段每层的工作量也都是 O(n),共有 log n 层,所以合并阶段总时间复杂度 = O(n) × O(log n) = O(n log n)

5.3 总时间复杂度
总时间复杂度 = 分解阶段 + 合并阶段 = O(n log n) + O(n log n) = O(2n log n) = O(n log n) (常数因子忽略)

六、两种实现方式的复杂度对比

6.1 递归实现(自顶向下)
class Solution {
    public ListNode sortList(ListNode head) {
        if (head == null || head.next == null) return head;
        ListNode slow = searchMid(head); // O(n) 分解
        ListNode tmp = slow.next; 
        slow.next = null;
        ListNode left = sortList(head); // T(n/2)
        ListNode right = sortList(tmp); // T(n/2)
        return mergeList(left, right); // O(n) 合并
    }
}
  • 空间复杂度:O(log n)(递归调用栈)
6.2 迭代实现(自底向上)
class Solution {
    public ListNode sortList(ListNode head) {
        // ... 计算链表长度 length
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            // 每轮 subLength 翻倍,共 log n 轮
            ListNode cur = dummy.next;
            while (cur != null) {
                // 提取两个长度为 subLength 的子链表
                // 合并它们 O(n)
                // ...
            }
        }
    }
}
  • 空间复杂度:O(1)(满足题目进阶要求)

七、与其他排序算法的对比

排序算法最好情况平均情况最坏情况空间复杂度稳定性
归并排序O(n log n)O(n log n)O(n log n)O(n) 或 O(log n)稳定
快速排序O(n log n)O(n log n)O(n²)O(log n)不稳定
堆排序O(n log n)O(n log n)O(n log n)O(1)不稳定
插入排序O(n)O(n²)O(n²)O(1)稳定

归并排序的最大优势:无论输入数据如何,都能保证 O(n log n) 的时间复杂度。

八、常见疑问解答

Q1:为什么分解阶段也算时间复杂度?不是只算合并吗?

A:分解阶段虽然不涉及元素比较,但需要遍历链表寻找中点(searchMid 函数),这些遍历操作同样消耗时间,需要计入总时间复杂度。

Q2:为什么说是 O(n log n) 而不是 O(n)?

A:因为需要 log n 层操作,每层都要处理 n 个元素。以 8 个元素为例,需要 3 层,每层处理 8 个元素,总操作次数约为 24 次(n log n),而不是 8 次(n)。

Q3:递归实现的空间复杂度为什么是 O(log n)?

A:递归调用会使用系统栈,最深时递归 log n 层(因为每次规模减半),所以需要 O(log n) 的额外空间。

九、总结

归并排序 O(n log n) 的时间复杂度来源于其完美的分治结构:

  1. log n 层:每次将问题规模减半,形成高度为 log n 的递归树
  2. 每层 O(n):无论是分解阶段的寻找中点,还是合并阶段的比较合并,每层都需要处理所有 n 个元素
  3. 层数 × 每层工作量 = O(log n) × O(n) = O(n log n)

这种"层数 × 每层工作量"的分析方法,不仅适用于归并排序,也是理解其他分治算法(如快速排序、二分查找)复杂度的核心思维模型。

十、题解

/**
 * 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 slow = searchMid(head);
        ListNode tmp = slow.next; 
        slow.next = null;
        ListNode left = sortList(head);
        ListNode right = sortList(tmp);
        return mergeList(left, right).next;
    }

    public ListNode searchMid(ListNode head) {
        ListNode fast = head.next, slow = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    public ListNode mergeList(ListNode head1, ListNode head2) {
        ListNode h = new ListNode();
        ListNode p = h;
        ListNode p1 = head1, p2 = head2;
        while (p1 != null && p2 != null) {
            if (p1.val <= p2.val) {
                p.next = p1;
                p1 = p1.next;
            } else {
                p.next = p2;
                p2 = p2.next;
            }
            p = p.next;
        }
        p.next = (p1 == null ? p2 : p1);
        return h;
    }
}
/**
 * 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;
        }
        int length = 0;
        ListNode node = head;
        while (node != null) {
            length++;
            node = node.next;
        }
        ListNode dummy = new ListNode(0, head);
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            ListNode pre = dummy;
            ListNode cur = dummy.next;
            while (cur != null) {
                ListNode p1 = cur;
                for (int i = 1; i < subLength && cur != null && cur.next != null; i++) {
                    cur = cur.next;
                }
                ListNode p2 = cur.next; 
                cur.next = null; 
                cur = p2;
                for (int i = 1; i < subLength && cur != null && cur.next != null; i++) {
                    cur = cur.next;
                }
                ListNode next = null;
                if (cur != null) {
                    next = cur.next; 
                    cur.next = null;
                }
                ListNode mergeList = mergeTwoList(p1, p2);
                pre.next = mergeList;
                while (pre.next != null) {
                    pre = pre.next;
                }
                cur = next;
            }
        }
        return dummy.next;
    }

    public ListNode mergeTwoList(ListNode head1, ListNode head2) {
        ListNode h = new ListNode();
        ListNode p = h;
        ListNode p1 = head1, p2 = head2;
        while (p1 != null && p2 != null) {
            if (p1.val <= p2.val) {
                p.next = p1;
                p1 = p1.next;
            } else {
                p.next = p2;
                p2 = p2.next;
            }
            p = p.next;
        }
        p.next = (p1 == null ? p2 : p1);
        return h.next;
    }
}

目录

  1. 归并排序时间复杂度 O(nlogn) 解析:LeetCode 148 排序链表
  2. 一、引言
  3. 二、问题背景:LeetCode 148.排序链表
  4. 三、归并排序的核心思想:分而治之
  5. 四、为什么是 O(nlogn)?从二叉树的角度理解
  6. 4.1 分解过程形成一棵"递归树"
  7. 4.2 合并过程自底向上构建有序链表
  8. 五、时间复杂度逐层剖析
  9. 5.1 分解阶段的工作量
  10. 5.2 合并阶段的工作量
  11. 5.3 总时间复杂度
  12. 六、两种实现方式的复杂度对比
  13. 6.1 递归实现(自顶向下)
  14. 6.2 迭代实现(自底向上)
  15. 七、与其他排序算法的对比
  16. 八、常见疑问解答
  17. Q1:为什么分解阶段也算时间复杂度?不是只算合并吗?
  18. Q2:为什么说是 O(n log n) 而不是 O(n)?
  19. Q3:递归实现的空间复杂度为什么是 O(log n)?
  20. 九、总结
  21. 十、题解
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 大模型:人工智能前沿技术与应用详解
  • AI 大模型开发实战指南:从基础储备到项目部署
  • Dify MCP Server 将工作流发布为第三方可调用服务
  • Flutter Web 开发:解决跨域(CORS)问题的方法
  • Codex 免费模型配置与实战:从安装到 MCP 扩展
  • AI 绘画模型格式转换完全指南:从问题诊断到场景化解决方案
  • 学术论文润色与降低AIGC检测率的提示词指令集
  • Verilog 语法详解:从入门到精通
  • 基于腾讯云轻量应用服务器部署 OpenClaw 并接入 QQ 与飞书机器人
  • 鸿蒙 APP 开发实战:网络请求与数据持久化
  • 数据结构复习:二叉树的概念、性质与遍历实现
  • AI 绘画提示词网站:从技术选型到生产环境部署实战
  • 2024 年中国 AI 大模型场景应用趋势蓝皮书
  • 基于 Stable Diffusion 与 YOLOv5 的智能安防原型搭建实战
  • 小厂架构师 AI Agent 落地实战:从全能幻想到最小可用场景
  • PyMySQL 详解:Python 连接 MySQL 数据库实战
  • ClawdBot 本地化语音转写与多语言翻译端到端实战
  • 基于 Python Flask 的小区物业管理系统设计与实现
  • OpenClaw 底层原理深度拆解:从指令到执行的本地 AI 引擎
  • 基于 Go 的电子病历智能助手与 HIS 对接实战

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online