news 2026/5/1 7:13:55

归并排序的趟数和时间复杂度

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
归并排序的趟数和时间复杂度

一、归并排序的趟数

归并排序的核心是分治思想:先把数组递归地分成两半(分),直到每个子数组只有 1 个元素;再把相邻的子数组合并成有序数组(治)。这里的 “趟数”,本质是合并阶段的轮次(也等于拆分阶段的递归深度)。

1. 趟数的计算逻辑

假设待排序数组的元素个数为n

  • 拆分阶段:每次将数组分成 2 份,直到子数组长度为 1。这个过程的次数等于以 2 为底的 n 的对数(向上取整或向下取整,最终结果一致),数学表示为⌊log₂n⌋⌈log₂n⌉(也可简化为log₂n,因为时间复杂度中对数的底数不影响阶数)。
  • 合并阶段:每一轮合并都是将当前的子数组两两合并,轮次和拆分的次数完全相同,这就是归并排序的趟数
2. 举例说明
  • 例 1:n=8(2³)拆分过程:8 → 4+4 → 2+2+2+2 → 1+1+…+1(共 8 个 1)。拆分次数(趟数):log₂8=3趟。
  • 例 2:n=7(非 2 的幂)拆分过程:7 → 3+4 → 1+2+2+2 → 1+1+1+1+1+1+1。拆分次数(趟数):log₂7≈2.8,向上取整为3 趟(或向下取整⌊log₂7⌋=2,但实际合并时仍需 3 趟,因此通常统一表述为log₂n趟,忽略小数部分的影响)。

结论:归并排序的趟数为 **log2​n**(n为元素个数,对数的底数为 2)。

二、归并排序的时间复杂度

归并排序的时间复杂度需要从拆分阶段合并阶段分别分析,最终结合得出整体复杂度。

1. 拆分阶段的时间复杂度

拆分操作只是将数组分成两半,没有元素的比较和交换,仅涉及索引的计算,时间复杂度为O(1)(每一次拆分),整个拆分阶段的总时间复杂度为 O (log n)(因为拆分次数是 log n 趟),相对于合并阶段可以忽略。

2. 合并阶段的时间复杂度

合并阶段是归并排序的核心,每一趟合并都需要遍历所有 n 个元素(将两两子数组合并时,每个元素都会被处理一次)。

  • 每一趟合并的时间复杂度:O (n)。
  • 总趟数:log n。
  • 因此,合并阶段的总时间复杂度:O (n × log n)。
3. 不同情况下的时间复杂度

归并排序是稳定的排序算法(相同值的元素相对位置不变),且其时间复杂度不受原始数组有序性的影响:

  • 最好情况:O (n log n)(原始数组已有序)。
  • 最坏情况:O (n log n)(原始数组逆序)。
  • 平均情况:O (n log n)。
补充:空间复杂度

归并排序需要额外的辅助数组来存储合并后的元素,因此空间复杂度为O(n)(这是归并排序的主要缺点,相比快速排序的 O (log n) 空间复杂度更高)。

三、代码示例(验证趟数和复杂度)

下面是 Python 实现的归并排序,添加了趟数统计的功能,帮助你直观看到趟数:

python

运行

def merge_sort(arr): # 统计趟数(使用非局部变量,避免全局变量) global merge_pass # 递归终止条件:子数组长度为1 if len(arr) <= 1: return arr # 拆分:将数组分成左右两半 mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) # 合并:将两个有序子数组合并 merge_pass += 1 # 每合并一次,趟数+1(这里统计的是合并操作的次数,总趟数为log2n) return merge(left, right) def merge(left, right): result = [] i = j = 0 # 双指针遍历两个子数组,按序添加到结果 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 # 添加剩余元素 result.extend(left[i:]) result.extend(right[j:]) return result # 测试 if __name__ == "__main__": # 初始化趟数 merge_pass = 0 arr = [3, 1, 4, 1, 5, 9, 2, 6] sorted_arr = merge_sort(arr) print("排序后的数组:", sorted_arr) print("合并操作的次数(趟数相关):", merge_pass) # 对于n=8,输出7(合并次数),趟数为3(log28=3)

说明:代码中merge_pass统计的是合并操作的次数(比如 n=8 时,合并次数是 7),而我们说的 “趟数” 是合并的轮次(n=8 时是 3 轮),两者的关系是:第 k 趟的合并次数为n/2k,总合并次数为n−1(比如 8-1=7)。

总结

  1. 趟数:归并排序的趟数为 **log2​n**(n 为元素个数,即拆分 / 合并的轮次),若 n 不是 2 的幂,趟数为向上取整后的log2​n。
  2. 时间复杂度:最好、最坏、平均情况均为O(n log n),这是归并排序的核心优势(不受原始数组有序性影响)。
  3. 补充:归并排序的空间复杂度为O(n),因为需要辅助数组存储合并后的元素。

合并阶段是归并排序的核心,每一趟合并都需要遍历所有 n 个元素 讲讲

要理解 “归并排序的每一趟合并都需要遍历所有 n 个元素”,我们需要先明确 “” 的定义(合并阶段的 “轮次”),再结合具体例子拆解每一趟的合并过程 —— 核心逻辑是:每一趟合并的目标是将当前所有 “有序子数组” 两两合并,而这些子数组的总长度恰好是 n,因此遍历的总元素数必然是 n

第一步:先明确 “合并阶段的趟” 是什么

归并排序的 “合并阶段”,是从 “最小有序子数组”(长度为 1 的子数组,天然有序)开始,每一轮(一趟)都将所有相邻的 “同长度有序子数组” 两两合并成 “长度翻倍的有序子数组”,直到最终合并成一个长度为 n 的有序数组。

这里的 “趟”=“合并轮次”,每一趟的核心特征是:

  • 趟内所有待合并的子数组长度相同(记为len);
  • 合并后生成的子数组长度为2*len
  • 每一趟处理的子数组总数是 “当前子数组个数”,但它们的总长度始终是 n。

第二步:用具体例子拆解 “每趟遍历 n 个元素”

n=8(元素:[3,1,4,1,5,9,2,6])为例,合并阶段共 3 趟(因为log₂8=3),我们逐趟分析:

第 1 趟合并:子数组长度 = 1 → 合并后长度 = 2
  • 初始状态:拆分阶段结束后,得到 8 个长度为 1 的有序子数组(每个子数组只有 1 个元素,天然有序):[3], [1], [4], [1], [5], [9], [2], [6]
  • 合并逻辑:将相邻的两个子数组两两合并,共需要合并 4 次(8 个→4 个):
    1. 合并[3][1][1,3](遍历 2 个元素);
    2. 合并[4][1][1,4](遍历 2 个元素);
    3. 合并[5][9][5,9](遍历 2 个元素);
    4. 合并[2][6][2,6](遍历 2 个元素);
  • 第 1 趟总遍历元素数:2+2+2+2 = 8 = n;
  • 第 1 趟结束后:得到 4 个长度为 2 的有序子数组:[1,3], [1,4], [5,9], [2,6]
第 2 趟合并:子数组长度 = 2 → 合并后长度 = 4
  • 当前状态:4 个长度为 2 的有序子数组,总长度仍为 8;
  • 合并逻辑:继续相邻两两合并,共需要合并 2 次(4 个→2 个):
    1. 合并[1,3][1,4]
      • 双指针遍历两个子数组,依次取较小元素:1(来自第二个子数组)→1(第一个)→3(第一个)→4(第二个),最终得到[1,1,3,4](遍历 4 个元素);
    2. 合并[5,9][2,6]
      • 双指针遍历:2(第二个)→5(第一个)→6(第二个)→9(第一个),最终得到[2,5,6,9](遍历 4 个元素);
  • 第 2 趟总遍历元素数:4+4 = 8 = n;
  • 第 2 趟结束后:得到 2 个长度为 4 的有序子数组:[1,1,3,4], [2,5,6,9]
第 3 趟合并:子数组长度 = 4 → 合并后长度 = 8(最终数组)
  • 当前状态:2 个长度为 4 的有序子数组,总长度还是 8;
  • 合并逻辑:最后一次两两合并(2 个→1 个):合并[1,1,3,4][2,5,6,9]
    • 双指针遍历:1(第一个)→1(第一个)→2(第二个)→3(第一个)→4(第一个)→5(第二个)→6(第二个)→9(第二个),最终得到[1,1,2,3,4,5,6,9](遍历 8 个元素);
  • 第 3 趟总遍历元素数:8 = n;
  • 第 3 趟结束后:得到 1 个长度为 n 的有序数组,排序完成。

第三步:为什么 “每趟都必须遍历 n 个元素”?

核心原因有两个,结合上面的例子很容易理解:

  1. 子数组总长度不变:无论每趟的子数组长度是 1、2 还是 4,所有子数组的总长度始终是 n(因为合并只是 “组合子数组”,没有增加或减少元素);
  2. 合并需完整遍历子数组:要将两个有序子数组合并成一个有序数组,必须完整遍历这两个子数组的所有元素(否则无法保证合并后的数组有序)。因此,每趟合并时,所有子数组的元素都会被遍历一次,总遍历数就是 n。

第四步:延伸思考 ——n 不是 2 的幂时,是否仍满足 “每趟遍历 n 个元素”?

即使 n 不是 2 的幂(比如 n=7),结论依然成立。以 n=7 为例,合并阶段共 3 趟(log₂7≈2.8,向上取整为 3 趟):

  • 第 1 趟:7 个长度为 1 的子数组 → 合并 3 次(得到 3 个长度为 2 的子数组 + 1 个长度为 1 的子数组),总遍历元素数 = 2+2+2+1=7=n;
  • 第 2 趟:3 个长度为 2 的子数组 + 1 个长度为 1 的子数组 → 合并 2 次(得到 1 个长度为 4 的子数组 + 1 个长度为 2 的子数组 + 1 个长度为 1 的子数组?不,实际是 “优先合并同长度”,最终总遍历数仍为 7);
  • 第 3 趟:合并剩余子数组,总遍历数还是 7=n。

本质是:即使子数组长度不完全相同,每趟所有待合并子数组的总长度依然是 n,因此遍历的总元素数始终是 n

总结

“归并排序每一趟合并都遍历所有 n 个元素” 的核心逻辑可概括为:

  • 合并的本质是 “将当前所有有序子数组合并成更大的有序子数组”;
  • 所有子数组的总长度永远等于 n(元素没有增减);
  • 合并任意两个子数组都需要完整遍历它们的元素,因此每趟遍历的总元素数必然是 n。

这也是归并排序时间复杂度为 O (n log n) 的关键:共 log n 趟,每趟 O (n),总复杂度 O (n log n)。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 6:04:48

17亿参数挑战行业巨头:Salesforce CoDA模型颠覆代码生成范式

2025年1月&#xff0c;Salesforce AI Research团队的陈浩霖、王诗雨、秦灿等研究者联合发表了题为"CoDA: Coding LM via Diffusion Adaptation"的突破性研究。这项创新成果通过arXiv:2510.03270v1预印本平台向学界公开&#xff0c;开源代码与模型权重则可通过GitCode…

作者头像 李华
网站建设 2026/4/28 2:45:00

13、Unix系统文件操作与系统信息查询指南

Unix系统文件操作与系统信息查询指南 1. 文件分割:split命令的使用 在日常使用中,我们可能会遇到文件过大无法通过邮件发送的情况。例如,你想用新数码相机拍摄了新电脑的照片,想通过邮件分享给亲朋好友,但因文件太大,ISP无法发送。这时,除了修改文件本身(如减小物理尺…

作者头像 李华
网站建设 2026/4/23 12:35:01

21、深入探索函数与 `getline` 函数:从自定义函数到输入处理

深入探索函数与 getline 函数:从自定义函数到输入处理 1. 自定义函数的魅力 在编程的世界里,自定义函数是提升代码复用性和模块化程度的重要手段。 1.1 自定义函数基础 自定义函数允许程序员编写自包含的代码块,这些代码块可以在不同的程序中重复使用。函数定义的基本…

作者头像 李华
网站建设 2026/5/1 6:49:30

25、Awk编程:工具、应用与实战详解

Awk编程:工具、应用与实战详解 1. Awk工具概述 Awk是一种强大的文本处理语言,有多种不同的实现版本,各有特点和优势。 1.1 Michael的mawk mawk由Michael Brennan编写,与POSIX awk向上兼容,并且有一些扩展功能。它的主要优点是速度快且健壮,虽然功能比gawk少,但性能通…

作者头像 李华
网站建设 2026/5/1 6:50:46

31、Awk脚本语言快速参考

Awk脚本语言快速参考 1. 命令行语法 调用awk有两种基本形式: - awk [-v var=value] [-F re] [--] ’pattern { action }’ var=value datafile(s) - awk [-v var=value] [-F re] -f scriptfile [--] var=value datafile(s) 一个awk命令行由命令、脚本和输入文件名组成…

作者头像 李华