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

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

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

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函数:

publicListNodesearchMid(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函数:

publicListNodemergeList(ListNode head1,ListNode head2){ListNode h =newListNode();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 递归实现(自顶向下)

classSolution{publicListNodesortList(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)returnmergeList(left, right);// O(n)合并}}
  • 空间复杂度:O(log n)(递归调用栈)

6.2 迭代实现(自底向上)

classSolution{publicListNodesortList(ListNode head){// ...计算链表长度lengthfor(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; } * } *//** * 归并排序递归法 */classSolution{publicListNodesortList(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);returnmergeList(left, right).next;}publicListNodesearchMid(ListNode head){ListNode fast = head.next, slow = head;while(fast !=null&& fast.next !=null){ slow = slow.next; fast = fast.next.next;}return slow;}publicListNodemergeList(ListNode head1,ListNode head2){ListNode h =newListNode();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; } * } *//** * 自底向上的方法 */classSolution{publicListNodesortList(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 =newListNode(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;}publicListNodemergeTwoList(ListNode head1,ListNode head2){ListNode h =newListNode();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;}}

Read more

【Actix Web】Rust Web开发实战:Actix Web框架全面指南

【Actix Web】Rust Web开发实战:Actix Web框架全面指南

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,ZEEKLOG全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Rust开发,Python全栈,Golang开发,云原生开发,PyQt5和Tkinter桌面开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi,flask等框架,云原生K8S,linux,shell脚本等实操经验,网站搭建,数据库等分享。 所属的专栏:Rust语言通关之路 景天的主页:景天科技苑 文章目录 * Rust Web开发 * 一、Actix Web框架概述 * 1.1 Actix Web的特点 * 1.2 Actix Web与其他Rust框架比较

By Ne0inhk
本地服务器用 OpenClaw + Open WebUI 搭建企业多部门 AI 平台(附 Docker 避坑指南)

本地服务器用 OpenClaw + Open WebUI 搭建企业多部门 AI 平台(附 Docker 避坑指南)

引言: 最近在尝试使用 OpenClaw,发现这个 AI 个人助理框架非常有意思。于是团队里就有人提出:能不能为公司的多个部门,分别搭建专属的 OpenClaw 服务器? 诚然,现在有钉钉、飞书等成熟的办公软件可以接入 AI,但对于一些尚未全面普及此类协作软件的企业(或者需要绝对私有化部署的团队)来说,独立搭建一套内部 AI 门户依然是刚需。 起初,我们考虑直接让大家通过 OpenClaw 自带的 Web 界面进行跨电脑访问。但实操后发现这存在致命缺陷: 1. 权限越界:自带的 Web 端拥有底层的配置编辑权限,暴露给普通员工极其不安全。 2. 无法溯源:多终端共用一个 Web 界面,根本无法追溯对话是由谁发起的。 3. 缺乏隔离:无法按部门精细化分配 API 额度或限制特定部门只能访问特定的 OpenClaw 节点,无法实现业务隔离。 为了解决这些痛点,我们最终确定了这套架构方案:

By Ne0inhk

Flutter 三方库 web_ffi 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、全场景的 Web 浏览器 FFI(外部函数接口)与 WebAssembly 跨平台调用引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 web_ffi 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、透明、全场景的 Web 浏览器 FFI(外部函数接口)与 WebAssembly 跨平台调用引擎 在鸿蒙(OpenHarmony)系统的 Web 浏览器环境(Webview/Ohos Browser)开发高性能 Web 应用时,如何调用现有的 C/C++ 算法库(Wasm 格式)且能保持与原生 HAP 环境下的 dart:ffi 接口完全一致?web_ffi 为开发者提供了一套工业级的、基于 JS 绑定的

By Ne0inhk
oi~,让我告诉你如何实现哈希表

oi~,让我告诉你如何实现哈希表

1.哈希概念         哈希(hash)又称散列,是一种组织数据的方式。从译名来看有散乱排列的意思,其本质是通过哈希函数将关键字key值与存储位置建立映射关系,查找时通过哈希函数计算出key值的位置,实现快速查找。 1.1直接定址法         当关键字key值比较集中时,直接定址法是最简单也是最有效的方法。比如一组数据集中在[ 0 - 99 ],我们开一组大小为100的数组即可,数据直接作为下标来定位存储位置。再比如一组数据集中在[ a - z],我们开一个大小为26的数组即可,让数据减去字符‘a'的结果来定位存储位置。         例题:387. 字符串中的第一个唯一字符 - 力扣(LeetCode) class Solution { public: int firstUniqChar(string s) { int arr[26]={0}; //范围for 遍历 for(auto

By Ne0inhk