归并排序
1 算法执行
1.1 输入与输出
Funtion Name: INSERTION-SORT
Input: array of <a1, a2, ..., an>,其中需要排列索引从p到r的元素;
Output: array of <a1’, a2’, …, an’>, ap’<=a2’<=…<=ar’;
1.2 伪代码
MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1...n1 + 1] and R[1...n2 + 1] be new arrays
for i = 1 to n1
L[i] = A[p + i - 1]
for j = 1 to n2
R[j] = A[q + j]
L[n1 + 1] = Inf
R[n2 + 1] = Inf
i = 1
j = 1
for k = p to r
if L[i] <= R[j]
A[k] = L[i]
i = i + 1
else
A[k] = R[j]
j = j + 1
MERGE-SORT(A, p, r)
if p < r
q = [(p + r) / 2]
MERGE-SORT(A, p, q)
MERGE-SORT(A, q + 1, r)
MERGE(A, p, q, r)
1.3 C++代码
#include<iostream>
using namespace std;
#define lengthA 8
#define Inf 10
template<typename T>
void MergeSort(T A[], int p, int r);
template<typename T>
void Merge(T A[], int p, int q, int r);
template<typename T>
void Printf(T A[], int len);
template<typename T>
void MergeSort(T A[], int p, int r)
{
if (p < r)
{
int q = (int)floor((float)(p + r) / 2);
//cout << q << endl;
MergeSort<T>(A, p, q);
MergeSort<T>(A, q + 1, r);
Merge(A, p, q, r);
}
else
return;
}
template<typename T>
void Merge(T A[], int p, int q, int r)
{
int n1 = q - p + 1;
int n2 = r - q;
int L[lengthA], R[lengthA];
for (int i = 0; i < n1; i++)
L[i] = A[p + i];
for (int i = 0; i < n2; i++)
R[i] = A[q + i + 1];
L[n1] = R[n2] = Inf;
int i = 0;
int j = 0;
for (int k = p; k <= r; k++)
{
if (L[i] <= R[j])
{
A[k] = L[i];
i++;
}
else
{
A[k] = R[j];
j++;
}
}
}
int main()
{
int A[lengthA] = { 1,3,6,7,8,5,4,2 };
MergeSort<int>(A, 0, lengthA - 1);
Printf<int>(A, lengthA);
system("pause");
}
template<typename T>
void Printf(T A[], int len)
{
cout << "Array after MergeSort: " << endl;
for (int i = 0; i < len; i++)
{
cout << A[i] << "\t";
}
cout << endl;
}
1.4 运行结果
2 算法分析
2.1 分治法
归并算法本质上是一种递归结果算法,典型地遵循分治法思想。分治法指将原问题分解成几个规模较小但类似于原问题的子问题,递归的求解这些子问题,然后再合并子问题的解构成原问题的解。
分治模式在每层递归时都有三个步骤:
- 分解原问题为若干子问题,这些子问题都是原问题的规模较小的实例
- 解决这些子问题,递归的求解各子问题,然而,子问题规模足够小则直接求解
- 合并这些子问题的解,构成原问题的解
归并排序算法就是典型的分治模式,它可以被描述如下:
- 分解:
分解待排序的n个元素的序列成各具n/2个元素的两个子序列 - 解决:
使用归并排序递归的排序两个子序列 - 合并:
合并两个已排序的子序列以产生已排序答案
当序列被分为长度为1的子序列时,不需要排序,此时开始合并。
首先在分解时,分解的算法如下(先忽略最后一行):
MERGE-SORT(A, p, r)
if p < r
q = [(p + r) / 2]
MERGE-SORT(A, p, q)
MERGE-SORT(A, q + 1, r)
MERGE(A, p, q, r)
这一函数的意思表示对A中从p到r个元素进行排序。首先判定p和r的关系。如果p大于等于r,大于则说明程序出现错误,等于则说明数组已经只有一个元素了,不用继续排序。
当数组被分解为元素个数均为1的子数组时,此时可以执行合并。
合并的步骤举例而言,比如将一个n1长的子数组和n2长的子数组进行合并,假定这两个子数组均已排好序,则应该给出一个n1+n2长的数组作为归并的结果。现在比较两个子数组的第一个元素大小,取出最小的一个排在归并结果的第一位上。如果最小元素在n1长的子数组内,则不再比较这一元素(i++),将n1长的子数组的第二个元素与n2长的子数组的第一个元素比较大小,取出小的那个排在归并结果的第二位上,以此类推,直至其中一个子数组全部填入归并结果内,此时结束归并。
由于不知道何时结束归并(每次需要判决数组是否读完是很麻烦的),我们可以将归并的数组添加一个不可能作为最小值的数作为末尾(类似char[]的\0),因为我们需要填入归并结果内的数是已知的,当其中一个子数组全部使用完毕后,则下一个数组应该直接落下,即按序填入最后余下的空位内。那么就可以令这些没有填入的数和一个无穷大作比较,那么一定可以实现落下的效果。由此可以控制执行的次数而非判决来控制函数的结束。
这一归并的伪代码如下:
MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1...n1 + 1] and R[1...n2 + 1] be new arrays
for i = 1 to n1
L[i] = A[p + i - 1]
for j = 1 to n2
R[j] = A[q + j]
L[n1 + 1] = Inf
R[n2 + 1] = Inf
i = 1
j = 1
for k = p to r
if L[i] <= R[j]
A[k] = L[i]
i = i + 1
else
A[k] = R[j]
j = j + 1
2.2 循环不变式
显然,在归并排序内,每次归并的过程中蕴含一个循环不变式,即在执行r-p+1步中,都会保持归并结果是被排好序的,且元素是两个子数组的最小元素的集合。
书上是这么说的:
在开始第12~17行for循环的每次迭代时,子数组A[p…k-1]按从小到大的顺序包含L[1…n1+1]和R[1…n2+1]中的k-p个最小元素,进而L[i]和R[j]是各自所在数组中未被复制回数组A的最小元素。
- 初始化:循环的第一次迭代开始有k=p,所以子数组A[p…k-1]为控,空数组包含L和R内的k-p=0个最小元素。i=j=1,L[1]和R[1]也是各自所在数组中未被复制回数组A的最小元素。
- 保持:假设L[i]<=R[j](反之同理),此时L[i]是L数组中未被复制回数组A的最小元素。由于A[p…k-1]包含k-p个最小元素,所以经过判决后,A[p…k]将包含k-p+1个最小元素。增加k的值和i的值,更新了循环不变式。
- 终止:此时k=r+1,根据循环不变式,子数组A[p…k-1]即为A[p…r]且按从小到大的顺序包含L和R内k-p=r-p+1个最小元素。数组L和R一起包含r-p+3个元素,即除了两个无穷大值外,其余元素均已填入A内。
2.3 运行时间
显然,归并函数MERGE的运行时间为
Θ
(
n
)
\Theta(n)
Θ(n)。但是将其作为子函数添加在MERGE-SORT内时,其执行次数便不直观了。对于MERGE-SORT这样类似的递归函数,往往可以使用递归方程或递归式描述运行时间。
假定需要排序的元素个数为2的幂次(简化问题),此时每一步分解都会产生相同长度的子数组。
- 分解:分解只需要计算数组的中心位置,需要常量时间
- 解决,需要递归求解两个长度均为n/2的问题,需要 2 T ( n / 2 ) 2T(n/2) 2T(n/2)的运行时间
- 合并,合并时执行MERGE函数,需要 Θ ( n ) \Theta(n) Θ(n)时间
如何得到递归方程呢?我们考虑一般化问题,假设 T ( n ) T(n) T(n)是规模为n的一个问题的运行时间。如果问题规模足够小,即对某个常量c,n<=c,则直接求解即可,运行时间为 Θ ( 1 ) \Theta(1) Θ(1)。假设将原问题分解为a个子问题,每个子问题的规模是原问题的1/b(归并上a=b=2,其他一般化问题则不一定),为了求解子问题,需要 T ( n / b ) T(n/b) T(n/b)的时间。则解决a个子问题需要 a T ( n / b ) aT(n/b) aT(n/b)时间。分解子问题还需要一定的时间,设为 D ( n ) D(n) D(n),合并需要 C ( n ) C(n) C(n),则可以得到递推式:
在归并算法中, a = b = 2 a=b=2 a=b=2, D ( n ) D(n) D(n)= Θ ( 1 ) \Theta(1) Θ(1), C ( n ) C(n) C(n)=c,带入后,有
运算后可以确定 T ( n ) = Θ ( n l g n ) T(n) = \Theta(nlgn) T(n)=Θ(nlgn)( l g n lgn lgn表示 log 2 ( n ) {\log _2}\left( n \right) log2(n))。
3 练习
2.3-1
2.3-2
一、更改最后的终止条件,每次比较后都判决每个子数组是否全部填入完毕(i, j == n1+1, n2+1),留下temp作为标志位后break,将另一个子数组的其他元素直接填入A之后。
MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1...n1] and R[1...n2] be new arrays
for i = 1 to n1
L[i] = A[p + i - 1]
for j = 1 to n2
R[j] = A[q + j]
i = 1
j = 1
temp = 0
for k = p to r
if L[i] <= R[j]
A[k] = L[i]
i = i + 1
else
A[k] = R[j]
j = j + 1
if i == n1 + 1
temp = 1
break
if j == n2 + 1
temp = 2
break
if temp != 0
if temp == 1
for l = k + 1 to r
A[l] = R[j]
j = j + 1
if temp == 2
for l = k + 1 to r
A[l] = L[i]
i = i + 1
二、直接更改第12行的for k = p to r,不使用for而改为while,将break条件作为while判决条件。
MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1...n1] and R[1...n2] be new arrays
for i = 1 to n1
L[i] = A[p + i - 1]
for j = 1 to n2
R[j] = A[q + j]
k = p
i = 1
j = 1
while i <= n1 and j <= n2
if L[i] <= R[j]
A[k] = L[i]
i = i + 1
k = k + 1
else
A[k] = R[j]
j = j + 1
k = k + 1
if i > n1
for l = k + 1 to r
A[l] = R[j]
j = j + 1
if j > n2
for l = k + 1 to r
A[l] = L[i]
i = i + 1
2.3-3
- 奠基:当 k = 1 k = 1 k=1时,有 T ( n ) = 2 = 2 l g 2 T(n) = 2 = 2lg2 T(n)=2=2lg2成立。
- 递推:假设当
n
1
=
2
k
n_1 = 2^k
n1=2k时有
T
(
n
1
)
=
n
1
l
g
n
1
T(n_1) = n_1lgn_1
T(n1)=n1lgn1成立,则当
n
=
2
k
+
1
=
2
n
1
n = 2^{k + 1} = 2n_1
n=2k+1=2n1时,有
T ( n ) = 2 n 1 lg n 1 + 2 n 1 = 2 n 1 ( lg n 1 + 1 ) = 2 n 1 lg ( 2 n 1 ) = n lg n \begin{array}{l} T\left( n \right) = 2{n_1}\lg {n_1} + 2{n_1}\\ = 2{n_1}\left( {\lg {n_1} + 1} \right)\\ = 2{n_1}\lg \left( {2{n_1}} \right)\\ = n\lg n \end{array} T(n)=2n1lgn1+2n1=2n1(lgn1+1)=2n1lg(2n1)=nlgn
证明完毕。
2.3-4
他的意思和归并排序类似,区别在于L和R的分解更改为L有n-1个元素,而R仅有一个元素。举例而言如下:
伪代码描述如下:
INSERTION(A, n)
let L[1...n] be the new array
for i = 1 to n - 1
L[i] = A[p + i - 1]
L[n] = Inf
R = A[n]
i = 1
for k = 1 to n
if L[i] <= R
A[k] = L[i]
i = i + 1
else
A[k] = R
INSERTION-SORT(A, n)
if n > 1
INSERTION-SORT(A, n - 1)
INSERTION(A, n)
- 分解:分解只需要单独取出最后一个元素,需要常量时间
- 解决,需要递归求解一个长度为n-1的问题,需要 T ( n − 1 ) T(n-1) T(n−1)的运行时间
- 合并,合并时执行INSERTION函数,需要 Θ ( n ) \Theta(n) Θ(n)时间
故递推式为
T
(
n
)
=
Θ
(
1
)
,
n
=
1
T(n) = \Theta(1),n = 1
T(n)=Θ(1),n=1
T
(
n
)
=
T
(
n
−
1
)
+
Θ
(
n
)
,
n
>
1
T(n) = T(n-1)+\Theta(n),n > 1
T(n)=T(n−1)+Θ(n),n>1