冒泡排序的一次自我救赎

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/young2415/article/details/84259231

快不一定就好,比如说。。。咳咳,你们懂得。但是在排序界,排序速度的快慢可以说是衡量一个算法好坏的重要指标。今天AP哥要给大家介绍的这一款排序算法,可以说是出了名的慢,以至于好像只在书上见过它,在实际应用中并没有它的影子,那就是冒泡排序。可是,它就真的一无是处吗?先别着急下结论,且听我慢慢道来。

首先我们来看一下冒泡排序的原理,然后你就知道为什么它这么不受待见了。

以整型数组A = { 19,14,10,4,15,26,20,96 }为例,冒泡排序步骤如下:

  1. i=A.lenA.len表示数组A的长度);
  2. i > 1时,重复步骤3、4、5、6
  3. j=0,当j < i-1时,重复步骤4、5
  4. A[j] > A[j+1],则交换A[j]A[j+1]的值;
  5. j的值加1
  6. 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
再加上它交换元素的值所花费的时间……所以冒泡排序的时间复杂度是 O ( n 2 ) O(n^2) ,这样看来,说冒泡排序速度慢好像也没冤枉它。不过,我们刚才看到的,只是它在最坏情况下的时间复杂度,也就是说直到进行了n-1趟排序,整个序列才被排好序。

冒泡排序图示.png

但是小伙伴们肯定已经发现了,在上面那张图中,第3趟排序结束后,序列就已经整体有序了,但是我们的算法并不在乎,它只是忠实地执行着我们的指令,第1趟排序让最大的元素就位,第2趟排序让第二大的元素就位……说到这儿,AP哥不禁为计算机的忠实所感动……要是人与人之间也能这么单纯就好了。
1.jpg

因此,每经过一趟排序,我们就要判断一下序列是否已经有序,如果已经有序,就退出排序。判断的方法也很简单,当这一趟排序没有进行元素交换时,就说明序列已经有序了,所以我们需要设置一个标记,通过这个标记来记录这一趟排序有没有交换元素。

代码实现如下:

#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");
}

改进后的冒泡排序.png

经过改进,减少了冒泡排序的趟数,从而减少了冒泡排序的所花费的时间,那么这样就是最优的冒泡排序了吗?并不是。

再仔细观察一下上图,不难发现,第1趟排序结束后,「15」~「96」之间的这几个数已经是有序的了,它们已经处在自己应该处的位置上了。既然第1趟排序结束后已经有5个元素就位,那么到了第2趟时我们只需要关注剩下的没有就位的元素就可以了。

将这个问题扩展一下,可以用下面这张图表示:
2018-11-18_223136.png

如果序列的状态像上图这样,右侧大半部分就已经有序,那么我们在每一趟的扫描时就不需要扫描到②处,而是仅扫描到①处就够了,因为我们只需要对乱序的那部分进行排序。

要做到这一点,在每趟排序中,就不能只记录是否发生了交换,而是要记录该趟排序中最右侧逆序对的位置,最右侧的逆序对表明了序列的有序程度,也表明该逆序对右侧的元素已经有序。这样,到了下一趟排序时,只需要扫描到这次记录的最右侧逆序对的位置就够了。

下面是代码实现:

#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");
}

再次改进后的冒泡排序.png

经过两次改进,冒泡排序性能提升了不少,然而这种改进只有遇到我们之前所说的情况时才会生效。换而言之,如果待排序的序列恰好是逆序排列,我们的这种改进对于算法性能的提升依然于事无补。在最坏情况下,冒泡排序的时间复杂度依然是 O ( n 2 ) O(n^2)

但是当序列基本有序时,冒泡排序只做很少趟数的扫描即可完成排序,时间复杂度甚至能达到 O ( n ) O(n) 。所以,一个算法的好坏通常不能一概而论,而是要看具体的情况,根据不同的情况选择不同的、适合的算法,才是聪明的选择。

最后,欢迎关注我的微信公众号:AProgrammer,更多干货等你来发现!
AProgrammer.jpg

猜你喜欢

转载自blog.csdn.net/young2415/article/details/84259231
今日推荐