算法整理:内排序篇-二路归并排序&线性时间排序

二路归并排序

归并排序是一种基于分治策略(参照算法整理:内排序篇-冒泡排序&快速排序及其改进))的算法,假设待排序序列具有 n n n 个元素,将其看做是 n n n 个有序的子序列,然后将这 n n n 个有序的子序列两两合并,产生新的 n 2 \frac{n}2 2n 个有序的子序列,然后将这 n 2 \frac{n}2 2n 个有序的子序列两两合并,以此类推,直到全部元素排序完毕。其过程如下图所示:
在这里插入图片描述

C++代码

//排序长度为10000的整数数组 
void MergeSort_Recursive(array<int, 10000> &list, int beg, int end);
void Merge(array<int, 10000> &list, int beg, int center, int end);
void MergeSort(array<int, 10000> &list) {
	int center = list.size() / 2;
	MergeSort_Recursive(list, 0, center);
	MergeSort_Recursive(list, center, list.size());
	Merge(list, 0, center, list.size());
}

void MergeSort_Recursive(array<int, 10000> &list, int beg, int end) {

	if (beg < end - 1) {//因为左闭右开所以减一
		int center = beg + (end - beg) / 2;
		MergeSort_Recursive(list, beg, center);
		MergeSort_Recursive(list, center, end);
		Merge(list, beg, center, end);
	}
}

void Merge(array<int, 10000> &list, int beg, int center, int end) {
	array<int, 10000> templist;
	int index = 0, left = 0, right = 0;
	while (beg + left < center && center + right < end) {
		if (list[beg + left] < list[center + right]) {
			templist[beg + index++] = list[beg + left++];
		}
		else {
			templist[beg + index++] = list[center + right++];
		}
	}
	while (beg + left < center)
	{
		templist[beg + index++] = list[beg + left++];
	}
	while (center + right < end)
	{
		templist[beg + index++] = list[center + right++];
	}
	for (int i = beg; i < end; i++) {
		list[i] = templist[i];
	}
}

算法分析
二路归并排序的递归深度为 l g n lgn lgn,而每层递归执行的操作的时间复杂度为 O ( n ) O(n) O(n),所以归并排序的时间复杂度为 O ( n l g n ) O(nlgn) O(nlgn)。另外归并排序需要占用与待排序序列相同的大小的辅助空间,所以它的空间复杂度为 O ( n ) O(n) O(n)

二路归并排序的非递归实现

归并排序的递归形式虽然简洁易懂,但并不实用,实际上,非递归形式的归并排序要更好一些。
C++代码

//排序长度为10000的整数数组 
void MergeSort_NoneRecursive(array<int, 10000> &list) {
	int subsequenceSize = 1;
	while (subsequenceSize < list.size())
	{
		int i = 0;
		for (i;  i + 2 * subsequenceSize < list.size(); i += 2 * subsequenceSize) {
			Merge(list, i, i + subsequenceSize, i + 2 * subsequenceSize);
		}
		if(i + subsequenceSize <list.size())Merge(list, i, i + subsequenceSize,list.size());
		subsequenceSize *= 2;
	}
}
//Merge操作与递归版本的一致

比较排序算法的下界

目前为止介绍的排序算法都是比较排序算法,即基于元素之间的比较进行排序。关于比较排序算法的下界,有两个有用的结论:

在最坏情况下,任何比较排序算法都需要做 Ω ( n l g n ) \Omega(nlgn) Ω(nlgn) 次比较。
堆排序和归并排序都是渐进最优的比较排序算法。

比较排序的下界可由决策树模型得出,其树高就是一个比较排序算法中最坏情况的比较次数。详细内容参照《算法导论》第8章,在此不再赘述。

线性时间的排序方法

接下来介绍三种线性时间复杂度的排序方法:计数排序、基数排序和桶排序。

计数排序

计数排序得思想很简单,对于每一个输入待排序元素,确定小于该元素的元素个数,然后根据这个信息直接把待排序元素放到正确的位置上。其步骤如图所示:
在这里插入图片描述
C++代码

//排序长度为10000的整数数组 
void RadixSort(array<int, 10000> &list) {
	array<int, 10001> templistA = { 0 };
	array<int, 10000> templistB;
	for (auto listitem : list) {
		templistA[listitem] ++;
	}
	for (int i = 1; i < templistA.size(); i++) {
		templistA[i] += templistA[i - 1];
	}

	for (int i = list.size() - 1; i >= 0; i--) {
		templistB[templistA[list[i]]-1] = list[i];
		templistA[list[i]]--;
	}
	list = templistB;
}

算法分析
计数排序要求输入的数据必须是有确定范围的整数,假设输入数据的范围为 0 0 0 k k k,根据下面的伪代码,2~3行初始化 C C C花费的时间为 θ ( k ) \theta(k) θ(k) ,4~5行计算每个数字的个数花费的时间为 θ ( n ) \theta(n) θ(n) ,7~8行计算元素的位置花费的时间为 θ ( k ) \theta(k) θ(k) ,10~12行填充有序数组花费的时间为 θ ( n ) \theta(n) θ(n) ,所以总的时间代价为 θ ( k + n ) \theta(k+n) θ(k+n)
在这里插入图片描述
计数排序的下界优于比较排序的下界,实际上计数排序完全没有输入元素之间的比较操作。但是计数排序消耗的辅助空间很大,空间复杂度为 O ( n + k ) O(n+k) O(n+k)。另外,计数排序是稳定的。

基数排序

基数排序是一种多关键字排序。

一般的,多关键字排序可以分为两种方式:第一种为最高位优先(MSD),即先排最高位,再排次高位。以排序3位整数为例,采用最高位排序需要先按照百位对元素排序,再按照十位排序,最后按照个位排序。第二种排序方式为最低位优先(LSD),与最高位优先正好相反,先排最低位,再排次低位。
按照MSD排序时通常需要将序列分为若干子序列,然后对子序列分别排序,而按照LSD排序时不需要分割序列,但是对每一位关键字排序时需要使用稳定的排序方法。

基数排序是最低位优先的排序方式,以排序数字为例,先按照个位排序整个序列,然后按照十位…最后按照最高位排序。排序的过程中需要保证对每一位关键字排序时使用的算法是稳定的,否则基数排序无法正常工作。(使用前面提到的计数排序即可),排序过程如图所示:
在这里插入图片描述
C++代码

//排序长度为10000的整数数组 
void countingsort(array<int, 10000> &list, int digit);
void RadixSort(array<int, 10000> &list) {
	for (int i = 1; i <= 6; i++) {
		countingsort(list, i);
	}
}

void countingsort(array<int, 10000> &list, int digit) {
	array<int, 11> templistA = { 0 };
	array<int, 10000> templistB;
	int a = 1, b = 1;
	for (int i = 0; i < digit; i++) a *= 10;
	for (int i = 1; i < digit; i++)b *= 10;
	for (auto listitem : list) {
		templistA[listitem % a / b] ++;
	}
	for (int i = 1; i < templistA.size(); i++) {
		templistA[i] += templistA[i - 1];
	}
	for (int i = list.size() - 1; i >= 0; i--) {
		templistB[templistA[list[i] % a / b] - 1] = list[i];
		templistA[list[i] % a / b]--;
	}
	list = templistB;
}

算法分析

排序 n n n d d d位数,每个数位有 k k k种取值时,如果基数排序使用的稳定排序方法耗时 θ ( n + k ) \theta(n+k) θ(n+k),则基数排序排序整个序列的时间代价为 θ ( d ( n + k ) ) \theta(d(n+k)) θ(d(n+k))

这个结论很直观,对于有 d d d个关键字的序列,基数排序一共要排 d d d趟,如果排序一趟需要 θ ( n + k ) \theta(n+k) θ(n+k),那么排序d趟需要 θ ( d ( n + k ) ) \theta(d(n+k)) θ(d(n+k))。另外基数排序需要 O ( n + k ) O(n+k) O(n+k)的空间复杂度。

桶排序

桶排序假设输入数据服从均匀分布。
桶排序的步骤如下:首先分配 n n n个空桶,然后把元素分配到相应的桶中,最后对每个桶分别进行排序。
图片来自百度
C++代码

//排序长度为10000的整数数组 
void InsertionSort(vector<int> &list);
const int BucketSize = 100;
void BucketSort(array<int, 10000> &list) {
	array<vector<int>, 101> bucket;
	for (auto item : list) {
		bucket[item / 100].push_back(item);
	}
	for (auto &item:bucket) {
		InsertionSort(item);
	}
	int index = 0;
	for (auto item : bucket) {
		for (auto val:item) {
			list[index] = val;
			index++;
		}
	}
}

void InsertionSort(vector<int> &list) {
	for (int i = 1; i < list.size(); i++) {
		int sentry = list[i];
		int j = i;
		for (j; j > 0 && sentry < list[j - 1]; j--) {
			list[j] = list[j - 1];
		}
		list[j] = sentry;
	}
}

算法分析
桶排序的时间代价服从下面这个式子:
T ( n ) = θ ( n ) + ∑ i = 0 n − 1 O ( n 2 ) T(n)=\theta(n)+\sum_{i=0}^{n-1}O(n^2) T(n)=θ(n)+i=0n1O(n2)
化简之后,可以得到桶排序的期望运行时间为 θ ( n ) \theta(n) θ(n)

总结

尽管相对于快排等比较排序方法,基数排序等线性时间排序在时间复杂度上看起来要好一些,但是这并不意味着基数排序就比快速排序好。首先这些算法时间复杂度的常数因子不同,其次它们对主存空间和底层硬件的需求也不同。我们应当根据具体问题和设备环境合理选择排序算法。

算法时间复杂度(最坏)时间复杂度(最好)时间复杂度(平均)空间复杂度稳定性
归并排序 O ( n l g n ) O(nlgn) O(nlgn) O ( n l g n ) O(nlgn) O(nlgn) O ( n l g n ) O(nlgn) O(nlgn) O ( n ) O(n) O(n)稳定
计数排序 O ( n + k ) O(n+k) O(n+k) O ( n + k ) O(n+k) O(n+k) O ( n + k ) O(n+k) O(n+k) O ( n + k ) O(n+k) O(n+k)稳定
基数排序 O ( d ( n + k ) ) O(d(n+k)) O(d(n+k)) O ( d ( n + k ) ) O(d(n+k)) O(d(n+k)) O ( d ( n + k ) ) O(d(n+k)) O(d(n+k)) O ( n + k ) O(n+k) O(n+k)稳定
桶排序 O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( n ) O(n) O(n) O ( n + k ) O(n+k) O(n+k)稳定

参考文献

《算法导论》
《数据结构》(严蔚敏)
十大经典排序算法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值