给定一个问题,如何求解?首先查看最简单的实例能否求解,假如可以求解的话,下一步就是思考能否将大的实例分解成小的实例,以及能否将小的实例组合成为大的实例,如果都可以的话就称实例能归约 ,这个问题具有递归结构 ,可以设计递归算法进行求解
排序问题:对数组的归约
排序问题:
输入:一个包含 n n 个元素的数组 A [ 0.. n − 1 ] A [ 0.. n − 1 ] ,其中每个元素都是整数;
调整元素顺序后的数组 A A ,使得对任意的两个下标 0 ≤ i < j ≤ n − 1 0 ≤ i < j ≤ n − 1 ,有 A [ i ] ≤ A [ j ] A [ i ] ≤ A [ j ] 。
依据元素下标拆分数组:插入排序与归并排序
第一种拆分方案及插入排序算法
算法分析
我们只需执行一个简单操作即可将数组 A [ 0.. n − 1 ] A [ 0.. n − 1 ] 分解成两部分:前 n − 1 n − 1 个元素 A [ 0.. n − 2 ] A [ 0.. n − 2 ] ,以及单独一个元素 A [ n − 1 ] A [ n − 1 ] 。前 n − 1 n − 1 个元素组成一个小 的数组,是原给定实例的子实例。在将原给定实例拆分成子实例之后,我们假定子实例已被求解,对数组来说,所谓子实例的解就是已经排好序的小的数组 A [ 0.. n − 2 ] A [ 0.. n − 2 ] 。要想完成对整个数组 A [ 0.. n − 1 ] A [ 0.. n − 1 ] 的排序,我们只需将最后一个元素 A [ n − 1 ] A [ n − 1 ] 和小数组 A [ 0.. n − 2 ] A [ 0.. n − 2 ] 中的元素逐个比较,然后将 A [ n − 1 ] A [ n − 1 ] 插入到合适的位置即可。连续应用递归调用,最终会到达基始情形:当 n = 1 n = 1 时,数组 A A 只有一个元素,此时无需排序,直接返回即可。
时间复杂度:
T ( n ) = { 1 n = 1 T ( n − 1 ) + O ( n ) o t h e r w i s e T ( n ) = { 1 n = 1 T ( n − 1 ) + O ( n ) o t h e r w i s e
将上述递归式展开:
T ( n ) ≤ T ( n − 1 ) + c n ≤ T ( n − 2 ) + c ( n − 1 ) + c n ⋯ ≤ c + ⋯ + c ( n − 1 ) + c n = O ( n 2 ) T ( n ) ≤ T ( n − 1 ) + c n ≤ T ( n − 2 ) + c ( n − 1 ) + c n ⋯ ≤ c + ⋯ + c ( n − 1 ) + c n = O ( n 2 )
算法低效的原因是:在运行过程中,问题规模是呈线性下降的,即每次递归操作都是将规模为 n n 的问题分解成一个规模为 n − 1 n − 1 的子问题。
第二种拆分方案及归并排序算法
算法分析:
将大的数组 A [ 0.. n − 1 ] A [ 0.. n − 1 ] 按下标拆分成规模相同的两半,即 A [ 0.. ⌈ n 2 ⌉ − 1 ] A [ 0.. ⌈ n 2 ⌉ − 1 ] 和 A [ ⌈ n 2 ⌉ . . n − 1 ] A [ ⌈ n 2 ⌉ . . n − 1 ] ;每一半依然是数组,形式相同,但是规模变小,因此是原给定实例的子实例。通过迭代执行分解操作,即可使得问题规模呈指数形式下降。在使用递归调用将小的数组排好序之后,我们只需依据这两个已排好序的小的数组,“归并”(Merge)出整个数组。这里的归并包括两层意思:合并、以及排序。
归并过程:
循环不变量,是指关于程序行为的一个断言;这个断言在循环起始时成立,并且每执行一轮循环时都保持成立,因此可以推论出当循环结束时,断言必定成立。
时间复杂度分析
T ( n ) = { 1 n = 1 2 T ( n 2 ) + O ( n ) o t h e r w i s e = O ( n log n ) T ( n ) = { 1 n = 1 2 T ( n 2 ) + O ( n ) o t h e r w i s e = O ( n log n )
分而治之算法时间复杂度分析及Master定理
在分而治之算法中,一种常见的情况是将一个规模为 n n 的实例归约成 a a 个子实例,每个子实例规模都相同(设为 n b n b )。假如“组合”子实例解的时间开销是 O ( n d ) O ( n d ) ,则我们可将时间复杂度 T ( n ) T ( n ) 的递归关系及基始情形表示如下:
T ( n ) = { 1 n = 1 a T ( n b ) + O ( n d ) o t h e r w i s e T ( n ) = { 1 n = 1 a T ( n b ) + O ( n d ) o t h e r w i s e
对于子问题比较规整的情况,即每个子问题的规模都相同 ,T(n) 上界的显式表达式已被总结成 Master 定理:
T ( n ) = a T ( n b ) + O ( n d ) ≤ a T ( n b ) + c n d ≤ a ( a T ( n b 2 ) + c ( n b ) d ) + c n d ≤ ⋯ ≤ c n d ( 1 + a b d + ( a b d ) 2 + ⋯ + ( a b d ) log b n ) + a log b n = ⎧ ⎪ ⎨ ⎪ ⎩ O ( n log b a ) d < log b a O ( n log b a log n ) d = log b a O ( n d ) d > log b a T ( n ) = a T ( n b ) + O ( n d ) ≤ a T ( n b ) + c n d ≤ a ( a T ( n b 2 ) + c ( n b ) d ) + c n d ≤ ⋯ ≤ c n d ( 1 + a b d + ( a b d ) 2 + ⋯ + ( a b d ) log b n ) + a log b n = { O ( n log b a ) d < log b a O ( n log b a log n ) d = log b a O ( n d ) d > log b a
依据元素的值拆分数组-快速排序
算法分析
依据元素的数值将大数组拆分成小数组,即选定一个元素作“中心元”(Pivot),比中心元数值小的元素组成一个小数组,比中心元数值大的那些元素组成另一个小数组。
时间复杂度
称排序后的数组 A A 为 ~ A A ~ ,因此数组 &A$ 的最小元是 ~ A [ 0 ] A ~ [ 0 ] , 最大元是 ~ A [ n − 1 ] A ~ [ n − 1 ] ,中位数是 ~ A [ ⌈ n 2 ⌉ ] A ~ [ ⌈ n 2 ⌉ ] 。我们在选择中心元时可能面临如下两种情况:
(1) ~ A [ n − 1 ] A ~ [ n − 1 ] / ~ A [ 0 ] A ~ [ 0 ] : 这样只会生成一个子实例,规模减少了 1,呈线性降低。如果在每一次迭代都是如此选择的话, 运行过程就与 InsertionSort 算法相同,时间复杂度为:
T ( n ) = T ( n − 1 ) + O ( n ) = O ( n 2 ) T ( n ) = T ( n − 1 ) + O ( n ) = O ( n 2 )
(2) ~ A [ ⌈ n 2 ⌉ ] A ~ [ ⌈ n 2 ⌉ ] : 这样会生成两个子实例,每个子实例的规模都是原来的一半,呈指数下降。如果在每一次迭代都是如此选择的话,运行过程就与 MergeSort 算法相同,时间复杂度为:
T ( n ) = 2 T ( n 2 ) + O ( n ) = O ( n log n ) T ( n ) = 2 T ( n 2 ) + O ( n ) = O ( n log n )
证明运行时间的期望值依然是 O ( n log n ) O ( n log n )
Modified-QuickSort 算法只做了一点修改:随机选择一个元素做中心元之后,先检验一下这个中心元是否足够好;如果足够好,则继续执行后续的比较和排序,否则重新选择一个元素做中心元。所谓的中心元足够好,是指它位于A ̃的中间区域,即~ A [ ⌈ n 4 ⌉ ] ⋯ ~ A [ ⌈ 3 n 4 ⌉ ] A ~ [ ⌈ n 4 ⌉ ] ⋯ A ~ [ ⌈ 3 n 4 ⌉ ] ,修改后的算法时间复杂度为:
T ( n ) ≤ T ( n 4 ) + T ( 3 n 4 ) + 2 n ≤ ( T ( n 16 ) + T ( 3 n 16 ) + 2 n 4 ) + ( T ( 3 n 16 ) + T ( 9 n 16 ) + 2 3 n 4 ) + 2 n = ( T ( n 16 ) + T ( 3 n 16 ) ) + ( T ( 3 n 16 ) + T ( 9 n 16 ) ) + 2 n + 2 n ≤ ⋯ = O ( n log 4 3 n ) T ( n ) ≤ T ( n 4 ) + T ( 3 n 4 ) + 2 n ≤ ( T ( n 16 ) + T ( 3 n 16 ) + 2 n 4 ) + ( T ( 3 n 16 ) + T ( 9 n 16 ) + 2 3 n 4 ) + 2 n = ( T ( n 16 ) + T ( 3 n 16 ) ) + ( T ( 3 n 16 ) + T ( 9 n 16 ) ) + 2 n + 2 n ≤ ⋯ = O ( n log 4 3 n )
接下来我们分析 QuickSort 算法的时间复杂度。在做具体的分析之前,我们先陈述关于运行时间的 3 点事实:
(1) 运行时间由比较次数界定:我们的目标就是计算期望运行时间 E [ X ] E [ X ] 。
(2) 任意两个元素 ~ A [ i ] A ~ [ i ] 和 ~ A [ j ] A ~ [ j ] 最多只会比较一次
(3) 两个元素~ A [ i ] A ~ [ i ] 和 ~ A [ j ] A ~ [ j ] 发生比较的概率是2 j − i + 1 2 j − i + 1
P r [ ~ A [ i ] 与 ~ A [ j ] 进 行 比 较 ] = 1 n + 1 n + n − ( j − i + 1 ) n × 2 j − i + 1 = ( j − i + 1 n + n − ( j − i + 1 ) n ) × 2 j − i + 1 = 2 j − i + 1 P r [ A ~ [ i ] 与 A ~ [ j ] 进 行 比 较 ] = 1 n + 1 n + n − ( j − i + 1 ) n × 2 j − i + 1 = ( j − i + 1 n + n − ( j − i + 1 ) n ) × 2 j − i + 1 = 2 j − i + 1
由此计算时间复杂度为:
E [ X ] = E [ n − 1 ∑ i = 0 n − 1 ∑ j = i + 1 X i j ] = n − 1 ∑ i = 0 n − 1 ∑ j = i + 1 E [ X i j ] = n − 1 ∑ i = 0 n − 1 ∑ j = i + 1 2 j − i + 1 = n − 1 ∑ i = 0 n − i − 1 ∑ k = 1 2 k + 1 ≤ n − 1 ∑ i = 0 n − 1 ∑ k = 1 2 k + 1 = O ( n log n ) E [ X ] = E [ ∑ i = 0 n − 1 ∑ j = i + 1 n − 1 X i j ] = ∑ i = 0 n − 1 ∑ j = i + 1 n − 1 E [ X i j ] = ∑ i = 0 n − 1 ∑ j = i + 1 n − 1 2 j − i + 1 = ∑ i = 0 n − 1 ∑ k = 1 n − i − 1 2 k + 1 ≤ ∑ i = 0 n − 1 ∑ k = 1 n − 1 2 k + 1 = O ( n log n )
空间复杂度
需要创建两个辅助数组 S − S − 和 S + S + ,这样一来,除了数组本身之外还要额外占用 n n 个内存单元,导致当 n n 比较大时,内存需求有时难以满足。
所以有了原位排序算法,为避免开辟辅助数组 S − S − 和 S + S + ,Lomuto 算法直接将数组 A 的左半部分当做 S − S − ,存放比中心元小的元素;把数组 A 的右半部分当做 S + S + ,存放比中心元大的元素
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异