快不一定就好,比如说。。。咳咳,你们懂得。但是在排序界,排序速度的快慢可以说是衡量一个算法好坏的重要指标。今天AP哥要给大家介绍的这一款排序算法,可以说是出了名的慢,以至于好像只在书上见过它,在实际应用中并没有它的影子,那就是冒泡排序。可是,它就真的一无是处吗?先别着急下结论,且听我慢慢道来。
首先我们来看一下冒泡排序的原理,然后你就知道为什么它这么不受待见了。
以整型数组A = { 19,14,10,4,15,26,20,96 }
为例,冒泡排序步骤如下:
- 令
i=A.len
(A.len
表示数组A的长度); - 当
i > 1时
,重复步骤3、4、5、6 - 令
j=0
,当j < i-1
时,重复步骤4、5 - 若
A[j] > A[j+1]
,则交换A[j]
和A[j+1]
的值; - 令
j
的值加1
; - 令
i
的值减1
;
用代码实现如下:
#include <iostream>
using namespace std;
void swap(int &a, int &b) {
a = a ^ b;
b = a ^ b;
a = a ^ b;
/*如果是其他类型,应该选择下面这种交换方式*/
//int temp = a;
//a = b;
//b = temp;
}
/*A:数组首地址
low:要排序的序列的第一个元素的位置
high:要排序的序列的最后一个元素的位置*/
void bubbleSort(int *A, int low, int high) {
for (int i = high; i > low; i--) {
for (int j = low; j < i; j++) {
if (A[j] > A[j + 1]) {
swap(A[j], A[j + 1]);
}
}
}
}
int main() {
int A[8] = { 19,14,10,4,15,26,20,96 };
bubbleSort(A, 0, 7);
for(int i = 0; i < 8; i++)
cout << A[i] << " ";
system("pause");
}
在冒泡排序中,每经过一趟交换(也就是步骤3到步骤5),就会有一个元素就位,所以如果一个序列有n个元素 的话,那么整个排序过程就要经过n-1趟交换。为什么是n-1趟排序而不是n趟呢?因为当第2个元素到第n个元素就位时,第1个元素自然也就位了,所以就用不着第n趟排序了。
从上述算法描述中我们可以看到:
- 第1趟排序要遍历n-1个元素
- 第2趟排序要遍历n-2个元素
- ……
所以整个排序过程要遍历的元素个数为:
(
n
−
1
)
+
(
n
−
2
)
+
⋅
⋅
⋅
+
1
=
∑
i
=
1
n
−
1
i
=
n
(
n
−
1
)
2
=
1
2
n
2
+
1
2
n
(n-1)+(n-2)+···+1=\sum_{i=1}^{n-1}i=\frac{n(n-1)}{2}=\frac{1}{2}n^2+\frac{1}{2}n
(n−1)+(n−2)+⋅⋅⋅+1=i=1∑n−1i=2n(n−1)=21n2+21n
再加上它交换元素的值所花费的时间……所以冒泡排序的时间复杂度是
O
(
n
2
)
O(n^2)
O(n2),这样看来,说冒泡排序速度慢好像也没冤枉它。不过,我们刚才看到的,只是它在最坏情况下的时间复杂度,也就是说直到进行了n-1趟排序,整个序列才被排好序。
但是小伙伴们肯定已经发现了,在上面那张图中,第3趟排序结束后,序列就已经整体有序了,但是我们的算法并不在乎,它只是忠实地执行着我们的指令,第1趟排序让最大的元素就位,第2趟排序让第二大的元素就位……说到这儿,AP哥不禁为计算机的忠实所感动……要是人与人之间也能这么单纯就好了。
因此,每经过一趟排序,我们就要判断一下序列是否已经有序,如果已经有序,就退出排序。判断的方法也很简单,当这一趟排序没有进行元素交换时,就说明序列已经有序了,所以我们需要设置一个标记,通过这个标记来记录这一趟排序有没有交换元素。
代码实现如下:
#include <iostream>
using namespace std;
void swap(int &a, int &b) {
a = a ^ b;
b = a ^ b;
a = a ^ b;
/*如果是其他类型,应该选择下面这种交换方式*/
//int temp = a;
//a = b;
//b = temp;
}
/*A:数组首地址
low:要排序的序列的第一个元素的位置
high:要排序的序列的最后一个元素的位置*/
void bubbleSort2(int *A, int low, int high) {
for (int i = high; i > low; i--) {
bool swaped = false; //记录是否发生过元素交换
for (int j = low; j < i; j++) {
if (A[j] > A[j + 1]) {
swap(A[j], A[j + 1]);
swaped = true;
}
}
if (swaped == false)
return;
}
}
int main() {
int A[8] = { 19,14,10,4,15,26,20,96 };
bubbleSort2(A, 0, 7);
for(int i = 0; i < 8; i++)
cout << A[i] << " ";
system("pause");
}
经过改进,减少了冒泡排序的趟数,从而减少了冒泡排序的所花费的时间,那么这样就是最优的冒泡排序了吗?并不是。
再仔细观察一下上图,不难发现,第1趟排序结束后,「15」~「96」之间的这几个数已经是有序的了,它们已经处在自己应该处的位置上了。既然第1趟排序结束后已经有5个元素就位,那么到了第2趟时我们只需要关注剩下的没有就位的元素就可以了。
将这个问题扩展一下,可以用下面这张图表示:
如果序列的状态像上图这样,右侧大半部分就已经有序,那么我们在每一趟的扫描时就不需要扫描到②处,而是仅扫描到①处就够了,因为我们只需要对乱序的那部分进行排序。
要做到这一点,在每趟排序中,就不能只记录是否发生了交换,而是要记录该趟排序中最右侧逆序对的位置,最右侧的逆序对表明了序列的有序程度,也表明该逆序对右侧的元素已经有序。这样,到了下一趟排序时,只需要扫描到这次记录的最右侧逆序对的位置就够了。
下面是代码实现:
#include <iostream>
using namespace std;
void swap(int &a, int &b) {
a = a ^ b;
b = a ^ b;
a = a ^ b;
/*如果是其他类型,应该选择下面这种交换方式*/
//int temp = a;
//a = b;
//b = temp;
}
/*A:数组首地址
low:要排序的序列的第一个元素的位置
high:要排序的序列的最后一个元素的位置*/
void bubbleSort3(int *A, int low, int high) {
while(low < high){
int last = low;//最右侧的逆序对初始化为[low, low+1]
for(int j = low; j < high; j++){
if (A[j] > A[j+1]){
last = j;
swap(A[j], A[j + 1]);
}
}
high = last;
}
}
int main() {
int A[8] = { 19,14,10,4,15,26,20,96 };
bubbleSort3(A, 0, 7);
for(int i = 0; i < 8; i++)
cout << A[i] << " ";
system("pause");
}
经过两次改进,冒泡排序性能提升了不少,然而这种改进只有遇到我们之前所说的情况时才会生效。换而言之,如果待排序的序列恰好是逆序排列,我们的这种改进对于算法性能的提升依然于事无补。在最坏情况下,冒泡排序的时间复杂度依然是 O ( n 2 ) O(n^2) O(n2)。
但是当序列基本有序时,冒泡排序只做很少趟数的扫描即可完成排序,时间复杂度甚至能达到 O ( n ) O(n) O(n)。所以,一个算法的好坏通常不能一概而论,而是要看具体的情况,根据不同的情况选择不同的、适合的算法,才是聪明的选择。
最后,欢迎关注我的微信公众号:AProgrammer,更多干货等你来发现!