本文介绍的是基于交换元素位置进行排序的两类排序算法——冒泡排序和快速排序。
冒泡排序
冒泡排序是一种基于交换的排序算法,它的思路很简单,假设待排序序列包含
n
n
n个元素,首先比较第1个元素和第2个元素,如果逆序,就将其交换。然后比较第2个和第3个元素,如果逆序,就将其交换,以此类推,直到比较完第
n
−
1
n-1
n−1个元素和第
n
n
n个元素。上述过程称为一次冒泡排序,在这次排序中,序列中最大的元素被排到末尾。接下来进行第二次冒泡排序,范围是第
1
1
1个元素和第
n
−
1
n-1
n−1个元素,将第二大的元素被排到
n
−
1
n-1
n−1的位置上。重复上面的过程,直到所有元素都排序完毕或一次排序过程中没有交换元素的操作。冒泡排序的流程如下图所示
C++代码
//排序长度为10000的整数数组
void BubbleSort(array<int, 10000> &list){
for (int i = 0; i < list.size()-1; i++) {
bool isSwap = false;
for (int j = 0; j < list.size() - i - 1; j++) {
if (list[j] > list[j + 1]) {
int temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
isSwap = true;
}
}
if (!isSwap)break;
}
}
算法分析
冒泡排序在最坏的情况下(待排序序列为逆序)需要进行
n
−
1
n-1
n−1次排序,
∑
i
=
n
2
(
i
−
1
)
=
n
(
n
−
1
)
2
\sum_{i=n}^2(i-1)=\frac{n(n-1)}2
∑i=n2(i−1)=2n(n−1)次比较,总的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
快速排序
快速排序是一种基于分治策略的算法。
分治策略
在分治策略中,我们将一些问题分解成较容易解决的子问题,如果划分后问题足够简单,则直接解决,反之继续分解成更小的子问题。在这种策略中,我们一般使用递归的方法解决一个问题,在每层递归中应用如下三个步骤:
- 分解:将问题划分为规模更小的子问题
- 解决:递归地求解出子问题,如果子问题的规模足够小,则停止递归,直接求解。
- 合并:将子问题的解组合成原文题的解
快速排序的基本思想为通过一次排序将待排序序列分成两部分,其中一部分序列中元素的值都比另一部分小,然后分别对这两部分序列继续排序,并以此类推,直到整个序列有序。
快速排序的算法流程如下:
- 选择一个元素作为分隔的基准点(pivot)
- 遍历待排序序列,将小于基准点的元素移到基准点左边,大于基准点的元素移到基准点的右边。
- 对小于基准点的部分和大于基准点的部分递归地使用1、2步排序,直到序列有序。
其中第2步的实现方法可以是这样的:
- 将基准点与待排序序列的首元素交换;
- 定义一个指针 index,用于指向序列中第一个大于基准点的元素的位置,将它初始化为指向序列中第二个元素的指针;
- 从序列中第二个元素开始,遍历序列,如果序列中元素小于基准点,则将该元素与index指向的元素交换,然后将index的指向后移一位;
- 遍历完成后,将index的指向前移一位,然后互换index指向的元素和首元素。
这个过程如下图所示
C++代码
//排序长度为10000的整数数组 选择首元素为分隔的基准点
int Partition(array<int, 10000> &list, int low, int high);
void QuickSortRecursive(array<int, 10000> &list, int low, int high);
void QuickSort(array<int, 10000> &list) {
int low = 0;
int high = list.size();
if (low < high) {
int pivot = Partition(list, low, high);
QuickSortRecursive(list, low, pivot);
QuickSortRecursive(list, pivot + 1, high);
}
}
void QuickSortRecursive(array<int, 10000> &list, int low, int high) {
if (low < high) {
int pivot = Partition(list, low, high);
QuickSortRecursive(list, low, pivot);
QuickSortRecursive(list, pivot + 1, high);
}
}
int Partition(array<int, 10000> &list, int low, int high) {
int pivot = low;
int index = pivot + 1;
for (int i = index; i < high; i++) {
if (list[i] < list[pivot]) {
int temp = list[i];
list[i] = list[index];
list[index] = temp;
index++;
}
}
int temp = list[index - 1];
list[index - 1] = list[pivot];
list[pivot] = temp;
return index - 1;
}
算法分析
快速排序的运行时间与算法运行时对待排序序列的划分相关,如果划分平衡(划分后两个数组的大小相近),则快速排序的性能较好,如果划分不平衡,则快速排序的性能较差。
其中最坏情况(每次划分产生的两个子问题分别包含
n
−
1
n-1
n−1个元素和
0
0
0个元素时)的时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。这里要注意待排序序列正序或者逆序都会造成快速排序的最坏情况。
而快速的平均情况和最好情况接近,其时间复杂度都为
O
(
n
l
g
n
)
O(nlgn)
O(nlgn)。
快速排序的随机化版本
前面提到过,待排序序列正序或者逆序都会造成快速排序的最坏情况,同样如果待排序序列几乎正序或几乎逆序也容易造成快速排序的最坏情况。为了缓解这个问题,我们可以在算法中引入随机性。
如何随机化?
- 而可以对输入序列进行随机排序,但是这样做并不高明,特别时在大数据输入的情况下,费时费力。
- 可以使用随机抽样的方法,即每次选择主元的时候选择随机位置的元素作为主元。
- 另外可以在第二步的基础上进行进一步改进,仍然采用随机抽样的方法,但是每次抽样的元素个数为三个,然后选择三个元素的中位数作为主元。(三数取中法)。
通过上述改进,我们可以期望大多数情况下岁输入序列的划分是比较均衡的。
C++代码
//排序长度为10000的整数数组 引入随机抽样,大部分代码没变化。
int Partition(array<int, 10000> &list, int low, int high);
void QuickSortRecursive(array<int, 10000> &list, int low, int high);
int getrandomPivot(array<int, 10000> &list, int low, int high);
void QuickSort(array<int, 10000> &list) {
int low = 0;
int high = list.size();
if (low < high) {
int pivot = Partition(list, low, high);
QuickSortRecursive(list, low, pivot);
QuickSortRecursive(list, pivot + 1, high);
}
}
void QuickSortRecursive(array<int, 10000> &list, int low, int high) {
if (low < high) {
int pivot = Partition(list, low, high);
QuickSortRecursive(list, low, pivot);
QuickSortRecursive(list, pivot + 1, high);
}
}
int Partition(array<int, 10000> &list, int low, int high) {
int pivot = getrandomPivot(list, low, high);
int temp = list[low];
list[low] = list[pivot];
list[pivot] = temp;
pivot = low;
int index = low + 1;
for (int i = index; i < high; i++) {
if (list[i] < list[pivot]) {
int temp = list[i];
list[i] = list[index];
list[index] = temp;
index++;
}
}
temp = list[index - 1];
list[index - 1] = list[pivot];
list[pivot] = temp;
return index - 1;
}
//用三数取中法随机选择主元素位置
int getrandomPivot(array<int, 10000> &list, int low, int high) {
default_random_engine e(time(0));
uniform_int_distribution<int> u(low, high - 1);//左闭右开
int a = u(e), b = u(e), c = u(e);
int m1 = list[a] < list[b] ? a : b;
int m2 = list[a] < list[c] ? a : c;
int m3 = list[b] < list[c] ? b : c;
if (m1 == m2) return m3;
else if (m1 == m3) return m2;
else return m1;
}
快速排序改进(与插入排序结合)
实际上对于一个几乎有序的输入序列进行排序时,插入排序的性能往往要高于快速排序。
当输入数据几乎有序时,插入排序速度很快,可以利用这一点提高快速排序的速度。对一个长度小于 k k k的子数组调用快速排序时,让它不做任何排序就返回,然后对整个数组执行插入排序。 k k k为超参数,需要自己设定。
C++代码
int Partition(array<int, 10000> &list, int low, int high);
void QuickSortRecursive(array<int, 10000> &list, int low, int high);
int getrandomPivot(array<int, 10000> &list, int low, int high);
const int K = 16;//
//插入排序
void InsertSort(array<int, 10000> &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;
}
}
//快速排序
void QuickSort(array<int, 10000> &list) {
int low = 0;
int high = list.size();
if (low < high) {
int pivot = Partition(list, low, high);
QuickSortRecursive(list, low, pivot);
QuickSortRecursive(list, pivot + 1, high);
}
InsertSort(list);
}
void QuickSortRecursive(array<int, 10000> &list, int low, int high) {
if (high - low < K) {
return;
}
else {
int pivot = Partition(list, low, high);
QuickSortRecursive(list, low, pivot);
QuickSortRecursive(list, pivot + 1, high);
}
}
int Partition(array<int, 10000> &list, int low, int high) {
int pivot = getrandomPivot(list, low, high);
int temp = list[low];
list[low] = list[pivot];
list[pivot] = temp;
pivot = low;
int index = low + 1;
for (int i = index; i < high; i++) {
if (list[i] < list[pivot]) {
int temp = list[i];
list[i] = list[index];
list[index] = temp;
index++;
}
}
temp = list[index - 1];
list[index - 1] = list[pivot];
list[pivot] = temp;
return index - 1;
}
int getrandomPivot(array<int, 10000> &list, int low, int high) {
default_random_engine e(time(0));
uniform_int_distribution<int> u(low, high - 1);//左闭右开
int a = u(e), b = u(e), c = u(e);
int m1 = list[a] < list[b] ? a : b;
int m2 = list[a] < list[c] ? a : c;
int m3 = list[b] < list[c] ? b : c;
if (m1 == m2) return m3;
else if (m1 == m3) return m2;
else return m1;
}
算法分析
改进后的算法期望时间复杂度为
O
(
n
k
+
n
l
g
(
n
k
)
)
O(nk+nlg(\frac{n}k))
O(nk+nlg(kn))。
k
k
k的值可以通过理论计算,但是一般通过实验进行选择。
快速排序的尾递归技术
实际上,快速排序中的第二个QuickSortRecursive可以被一个循环控制结构代替,这样做可以减少快速排序的栈深度。
栈深度是一次计算中会用到栈空间的最大值。快速排序中,每一层递归都会占用一定的栈空间。
C++代码
//仅给出改动的部分
int Partition(array<int, 10000> &list, int low, int high);
int getrandomPivot(array<int, 10000> &list, int low, int high);
void QuickSortRecursive_TailRecursive(array<int, 10000> &list, int low, int high);
const int K = 10;
void QuickSort_TailRecursive(array<int, 10000> &list) {
int low = 0;
int high = list.size();
while (low < high) {
if (high - low + 1 < K) {
break;
}
else {
int pivot = Partition(list, low, high);
QuickSortRecursive_TailRecursive(list, low, pivot);
low = pivot + 1;
}
}
InsertSort(list);
}
void QuickSortRecursive_TailRecursive(array<int, 10000> &list, int low, int high) {
while(low < high) {
if (high - low + 1 < K) {
return;
}
else {
int pivot = Partition(list, low, high);
QuickSortRecursive_TailRecursive(list, low, pivot);
low = pivot + 1;
}
}
}
总结
算法 | 时间复杂度(最坏) | 时间复杂度(最好) | 时间复杂度(平均) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | O ( 1 ) O(1) O(1) | 稳定 |
快速排序 | O ( n 2 ) O(n^2) O(n2) | 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) | 不稳定 |
参考文献
《算法导论》
《数据结构》(严蔚敏)
十大经典排序算法
至此排序篇(二)结束。以下内容并没有相关权威文献的考证,纯属个人猜想,有兴趣的同学仅做参考。
改进的快速排序K值的选择
C++ STL SGI版源码中K(源码中称为__stl_threshold)的值为16,为什么是16呢,下面理论上分析,当子序列的长度为k时,对一个子序列调用插入排序的最坏时间代价计算为:
k
2
2
\frac{k^2}2
2k2(逆序的情况下,遍历k个元素,每个元素的平均移动次序为
k
2
\frac{k}2
2k)
而平均代价为
k
2
4
\frac{k^2}4
4k2(假设平均情况下有一半的元素不用移动,此时遍历k个元素,每个元素的平均移动次序为
k
4
\frac{k}4
4k)
对一个子序列调用快速排序的期望时间复杂度为:
k
l
g
k
klgk
klgk那么要使算法时间代价更小,则有
k
2
4
⩽
k
l
g
k
=
>
k
4
⩽
l
g
k
=
>
k
⩽
16
\frac{k^2}4\leqslant klgk\;\;=>\;\;\frac{k}4\leqslant lgk\;\;=>\;\;k\leqslant16
4k2⩽klgk=>4k⩽lgk=>k⩽16所以
k
k
k 可以取16。