算法复习——分而治之篇之归并排序
以下内容主要参考中国大学MOOC《算法设计与分析》,墙裂推荐希望入门算法的童鞋学习!
1. 问题背景
杠铃增重问题:
每位参赛运动员向组委会提交排好序的三次试举重量,为便于杠铃拆卸,组委会需对所有试举重量递增排序。
那么组委会该如何根据试举重量安排杠铃增重顺序?
基本的解决方案:
- 选择排序:从待排序元素中迭代选出最小值并排序,在上面的例子中,需要比较66次;
- 插入排序:依次将每个元素插入到已排序序列之中,在上面的例子中,需要比较55次;
以上的解决方案都没有考虑到杠铃增重问题自身具有的特点,那就是局部有序性;但是,在上面的解决方案中,并没有利用到局部有序性。
基于局部有序性,可以提出一种快速合并的方法,比较两有序数组当前最小元素,将较小者逐一合入新数组。
基于该快速合并方法,也能产生两种后续策略:
-
逐一合并,比较27次:
-
两两合并,比较24次:
那么通过求解纲领增重问题的两两合并策略是否能启发我们开发一个很有效的排序策略。
2. 问题定义
排序问题(Sorting Problem)
输入:
- 包含 n n n个数字的序列 < a 1 , … , a n > <a_{1}, \dots, a_{n}> <a1,…,an>
输出:
- 输入序列的升序 < a 1 ′ , a 2 ′ , … , a n ′ > <a_{1}^{'}, a_{2}^{'}, \dots, a_{n}^{'}> <a1′,a2′,…,an′>,满足 a 1 ′ ≤ a 2 ′ ≤ ⋯ ≤ a n ′ a_{1}^{'} \leq a_{2}^{'} \leq \dots \leq a_{n}^{'} a1′≤a2′≤⋯≤an′
3. 问题变化
杠铃增重问题和排序问题有什么区别呢
3.1 问题输入变化
普通排序问题的输入如下:
杠铃增重问题的输入如下:
从中我们可以发现,普通排序问题是个完整的数组输入,中间不间断,而杠铃增重问题的输入是间断的;此外,杠铃增重问题具有局部有序性,而普通排序问题局部有序性缺失。
两者还是有区别,那么两两合并策略如何适应问题输入的变化?
3.2 解决输入变化
我们不妨尝试一下也将普通排序问题的输入进行分解,如下图所示。
这也是很常见的算法解决策略,可以把复杂的大问题分解成规模小一点但本质相同的问题。通过这种方法,我们解决了第一个问题变化,即通过分解可以处理完整数组输入的变化。
然后,我们可以继续分解操作,直到每个子数组只剩下了一个元素,我们发现分解至数组长度为1时天然有序,可以处理局部有缺失的变化。
由于子数组具有局部有序性,参考之前杠铃增重问题的解法,我们就可以通过两两合并的方法来构建有序数组。
4. 归并排序
4.1 算法流程
- 分解原问题:把数组 A [ 1.. n ] A[1..n] A[1..n]排序问题分解为 A [ 1 , ⌊ n 2 ⌋ ] A[1, \lfloor\frac{n}{2}\rfloor] A[1,⌊2n⌋]和 A [ ⌊ n 2 ⌋ + 1 , n ] A[\lfloor\frac{n}{2}\rfloor+1,n] A[⌊2n⌋+1,n]排序问题;
- 解决子问题:递归解决子问题得到两个有序的子数组;
- 合并问题解:将两个有序子数组合并为一个有序数组。
4.2 伪代码
初始调用MergeSort(A, 1, n)
MergeSort(A, left, right)
输入:数组A[1…n],数组下标left,right
输出:递增数组A[left…right]
if left >= right then
return A[left..right]
end
mid ← (left + right) // 2
MergeSort(A, left, mid)
MergeSort(A, mid+1, right)
Merge(A, left, mid, right)
return A[left..right]
Merge(A, left, mid, right)
输入:数组A[1…n],数组下标left,mid,right
输出:递增数组A[left…right]
A'[left..right] ← A[left..right]
i ← left, j ← mid + 1, k ← 0
while i <= mid and j <= right do
if A'[i] <= A'[j] then
A[left+k] ← A'[i]
k ← k + 1, i ← i + 1
end
else
A[left+k] ← A'[j]
k ← k + 1, j ← j + 1
end
end
if i <= mid then
A[left+k..right] ← A'[i..mid]
end
else
A[left+k..right] ← A'[j..right]
end
return A[left..right]
4.3 时间复杂度分析
显然, M e r g e Merge Merge的时间复杂度是 O ( n ) O(n) O(n)
假设完成
M
e
r
g
e
S
o
r
t
(
A
,
1
,
n
)
MergeSort(A, 1, n)
MergeSort(A,1,n)的运行时间是
T
(
n
)
T(n)
T(n)(为了便于分析,假设
n
n
n是2的幂),则根据其伪代码,易得
T
(
n
)
=
{
2
T
(
n
2
)
+
O
(
n
)
i
f
n
>
1
O
(
1
)
i
f
n
=
1
T(n)=\left\{ \begin{array}{rcl} 2T(\frac{n}{2})+O(n) & & {if\ n > 1}\\ O(1) & & {if\ n = 1} \end{array} \right.
T(n)={2T(2n)+O(n)O(1)if n>1if n=1
以上的递归式可以用主定理法很容易地求出
T
(
n
)
=
O
(
n
l
o
g
n
)
T(n)=O(nlogn)
T(n)=O(nlogn)。
当然,也可以用递归树法来求解,即用树的形式表示抽象递归。
这棵递归树每一层的代价都是 O ( n ) O(n) O(n),而这棵树共有 l o g ( n ) + 1 log(n)+1 log(n)+1层,所以两者相乘就是总的时间复杂度,即 T ( n ) = O ( n l o g n ) T(n)=O(nlogn) T(n)=O(nlogn)。
5. 分而治之
分而治之法的一般步骤(求解框架)就是:
- 分解原问题:原问题分解为多个子问题;
- 解决子问题:递归地求解各个子问题;
- 合并问题解:将结果合并为原问题解。