当前位置: 首页 > news >正文

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

一、归并排序的趟数

归并排序的核心是分治思想:先把数组递归地分成两半(分),直到每个子数组只有 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)。

http://www.jsqmd.com/news/90646/

相关文章:

  • 128K上下文模型遭遇“缩水“困境:用户实测6万字文本触发长度限制
  • 17亿参数挑战行业巨头:Salesforce CoDA模型颠覆代码生成范式
  • Python入门篇【函数巩固题】
  • 12、Unix 文件处理实用工具全解析
  • 13、Unix系统文件操作与系统信息查询指南
  • 14、Unix系统信息查询与环境配置全解析
  • 15、深入探究 zsh 和 bash 配置文件:从查看至定制
  • 16、Unix 环境配置:bash、ksh 和 csh 详细指南
  • 17、Unix 系统命令别名设置与作业管理全解析
  • 18、Unix 作业管理与脚本编写全解析
  • 智能云生态全景扫描:前沿技术应用与产业创新实践深度剖析
  • 19、Shell脚本的运行与高级应用技巧
  • 31、Unix 文件、目录与命令使用指南
  • 32、Unix系统操作与管理全解析
  • 本地部署指南:借助Ollama框架搭建GPT-OSS推理环境与交互式应用开发
  • 腾讯开源Hunyuan大模型系列:从边缘到云端的全场景AI解决方案
  • 哔哩下载姬DownKyi:5个简单步骤掌握B站视频批量下载
  • 3D开发者的宝藏地图:Objaverse-XL实战攻略
  • 48亿参数开源巨兽登场:Step1X-3D如何引爆3D内容生产的效率革命?
  • Mistral AI开源语音模型Voxtral震撼发布:多语言支持与成本优势重塑行业格局
  • 13、Sed脚本高级流控制与应用详解
  • 14、深入探索 awk 脚本编写
  • 15、Awk编程:表达式、系统变量及应用示例
  • 16、Awk编程:关系与布尔运算符、文件信息处理及格式化输出
  • 17、Awk编程:参数传递、信息检索与控制结构详解
  • 18、《编程中的条件语句、循环与数组应用》
  • 19、Awk编程:数组操作与实用技巧
  • 20、Awk 函数全面解析
  • 21、深入探索函数与 `getline` 函数:从自定义函数到输入处理
  • 22、Awk编程:文件、管道与菜单命令生成器的实用指南