二路归并排序
归并排序是一种基于分治策略(参照算法整理:内排序篇-冒泡排序&快速排序及其改进))的算法,假设待排序序列具有
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=0∑n−1O(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) | 稳定 |
参考文献
《算法导论》
《数据结构》(严蔚敏)
十大经典排序算法