码迷,mamicode.com
首页 > 编程语言 > 详细

冒泡排序的一次自我救赎

时间:2018-11-23 11:17:34      阅读:248      评论:0      收藏:0      [点我收藏+]

标签:smm   fmm   vhdx   vbs   pos   序列   tty   扩展   结束   

快不一定就好,比如说。。。咳咳,你们懂得。但是在排序界,排序速度的快慢可以说是衡量一个算法好坏的重要指标。今天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

用代码实现如下:

 1#include <iostream>
2
3using namespace std;
4
5void swap(int &a, int &b) {
6    a = a ^ b;
7    b = a ^ b;
8    a = a ^ b;
9
10    /*如果是其他类型,应该选择下面这种交换方式*/
11    //int temp = a;
12    //a = b;
13    //b = temp;
14}
15
16/*A:数组首地址
17  low:要排序的序列的第一个元素的位置
18  high:要排序的序列的最后一个元素的位置*/

19void bubbleSort(int *A, int low, int high) {
20    for (int i = high; i > low; i--) {
21        for (int j = low; j < i; j++) {
22            if (A[j] > A[j + 1]) {
23                swap(A[j], A[j + 1]);
24            }
25        }
26    }
27}
28
29int main() {
30    int A[8] = { 19,14,10,4,15,26,20,96 };
31    bubbleSort(A, 07);
32    for(int i = 0; i < 8; i++)
33        cout << A[i] << " ";
34    system("pause");
35}

在冒泡排序中,每经过一趟交换(也就是步骤3到步骤5),就会有一个元素就位,所以如果一个序列有n个元素 的话,那么整个排序过程就要经过n-1趟交换。为什么是n-1趟排序而不是n趟呢?因为当第2个元素到第n个元素就位时,第1个元素自然也就位了,所以就用不着第n趟排序了。

从上述算法描述中我们可以看到:

  • 第1趟排序要遍历n-1个元素
  • 第2趟排序要遍历n-2个元素
  • ……

所以整个排序过程要遍历的元素个数为:
技术分享图片
再加上它交换元素的值所花费的时间……所以冒泡排序的时间复杂度是技术分享图片,这样看来,说冒泡排序速度慢好像也没冤枉它。不过,我们刚才看到的,只是它在最坏情况下的时间复杂度,也就是说直到进行了n-1趟排序,整个序列才被排好序。

技术分享图片
冒泡排序图示.png

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

技术分享图片
1.jpg

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

代码实现如下:

 1#include <iostream>
2
3using namespace std;
4
5void swap(int &a, int &b) {
6    a = a ^ b;
7    b = a ^ b;
8    a = a ^ b;
9
10    /*如果是其他类型,应该选择下面这种交换方式*/
11    //int temp = a;
12    //a = b;
13    //b = temp;
14}
15
16/*A:数组首地址
17  low:要排序的序列的第一个元素的位置
18  high:要排序的序列的最后一个元素的位置*/

19void bubbleSort2(int *A, int low, int high) {
20    for (int i = high; i > low; i--) {
21        bool swaped = false//记录是否发生过元素交换
22        for (int j = low; j < i; j++) {
23            if (A[j] > A[j + 1]) {
24                swap(A[j], A[j + 1]);
25                swaped = true;
26            }
27        }
28        if (swaped == false)
29            return;
30    }
31}
32
33int main() {
34    int A[8] = { 19,14,10,4,15,26,20,96 };
35    bubbleSort2(A, 07);
36    for(int i = 0; i < 8; i++)
37        cout << A[i] << " ";
38    system("pause");
39}
技术分享图片
改进后的冒泡排序.png

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

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

将这个问题扩展一下,可以用下面这张图表示:

技术分享图片
2018-11-18_223136.png

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

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

下面是代码实现:

 1#include <iostream>
2
3using namespace std;
4
5void swap(int &a, int &b) {
6    a = a ^ b;
7    b = a ^ b;
8    a = a ^ b;
9
10    /*如果是其他类型,应该选择下面这种交换方式*/
11    //int temp = a;
12    //a = b;
13    //b = temp;
14}
15
16/*A:数组首地址
17  low:要排序的序列的第一个元素的位置
18  high:要排序的序列的最后一个元素的位置*/

19void bubbleSort3(int *A, int low, int high) {
20    while(low < high){
21        int last = low;//最右侧的逆序对初始化为[low, low+1]
22        for(int j = low; j < high; j++){
23            if (A[j] > A[j+1]){
24                last = j;
25                swap(A[j], A[j + 1]);
26            }
27        }
28        high = last;
29    }
30}
31
32int main() {
33    int A[8] = { 19,14,10,4,15,26,20,96 };
34    bubbleSort3(A, 07);
35    for(int i = 0; i < 8; i++)
36        cout << A[i] << " ";
37    system("pause");
38}
技术分享图片
再次改进后的冒泡排序.png

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

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

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

技术分享图片
微信公众号二维码.jpg

冒泡排序的一次自我救赎

标签:smm   fmm   vhdx   vbs   pos   序列   tty   扩展   结束   

原文地址:https://www.cnblogs.com/AProgrammer/p/10005941.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!