算法导论2.3

归并排序

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 分治法

归并算法本质上是一种递归结果算法,典型地遵循分治法思想。分治法指将原问题分解成几个规模较小但类似于原问题的子问题,递归的求解这些子问题,然后再合并子问题的解构成原问题的解。
分治模式在每层递归时都有三个步骤:

  1. 分解原问题为若干子问题,这些子问题都是原问题的规模较小的实例
  2. 解决这些子问题,递归的求解各子问题,然而,子问题规模足够小则直接求解
  3. 合并这些子问题的解,构成原问题的解

归并排序算法就是典型的分治模式,它可以被描述如下:

  1. 分解:
    分解待排序的n个元素的序列成各具n/2个元素的两个子序列
  2. 解决:
    使用归并排序递归的排序两个子序列
  3. 合并:
    合并两个已排序的子序列以产生已排序答案

当序列被分为长度为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的最小元素。

  1. 初始化:循环的第一次迭代开始有k=p,所以子数组A[p…k-1]为控,空数组包含L和R内的k-p=0个最小元素。i=j=1,L[1]和R[1]也是各自所在数组中未被复制回数组A的最小元素。
  2. 保持:假设L[i]<=R[j](反之同理),此时L[i]是L数组中未被复制回数组A的最小元素。由于A[p…k-1]包含k-p个最小元素,所以经过判决后,A[p…k]将包含k-p+1个最小元素。增加k的值和i的值,更新了循环不变式。
  3. 终止:此时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的幂次(简化问题),此时每一步分解都会产生相同长度的子数组。

  1. 分解:分解只需要计算数组的中心位置,需要常量时间
  2. 解决,需要递归求解两个长度均为n/2的问题,需要 2 T ( n / 2 ) 2T(n/2) 2T(n/2)的运行时间
  3. 合并,合并时执行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

  1. 奠基:当 k = 1 k = 1 k=1时,有 T ( n ) = 2 = 2 l g 2 T(n) = 2 = 2lg2 T(n)=2=2lg2成立。
  2. 递推:假设当 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)
  1. 分解:分解只需要单独取出最后一个元素,需要常量时间
  2. 解决,需要递归求解一个长度为n-1的问题,需要 T ( n − 1 ) T(n-1) T(n1)的运行时间
  3. 合并,合并时执行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(n1)+Θ(n)n>1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值