vlambda博客
学习文章列表

【算法】排序算法之冒泡排序


排序算法系列目录说明

  • 冒泡排序(Bubble Sort)

  • 插入排序(Insertion Sort)

  • 希尔排序(Shell Sort)

  • 选择排序(Selection Sort)

  • 快速排序(Quick Sort)

  • 归并排序(Merge Sort)

  • 堆排序(Heap Sort)

  • 计数排序(Counting Sort)

  • 桶排序(Bucket Sort)

  • 基数排序(Radix Sort)


排序算法简介说明

1.定义

将一组杂乱无章的数据按一定的规律顺次排列起来。例如:

 
   
   
 
  1. 输入:a1a2a3,…,an

  2. 输出:a1’,a2’,a3’,…,an’(满足a1 <= a2 <= a3 <= <= an’排列)

2.算法性能评估术语言

稳定: 如果a原本在b前面,而a=b时,排序之后a仍然在b的前面。不稳定: 如果a原本在b的前面,而a=b时,排序之后a可能出现在b的后面。

内排序: 所有排序操作都在内存中完成。外排序: 通常是由于数据太大,不能同时存放在内存中,根据排序过程的需要而在外存与内存之间 数据传输才能进行。

时间复杂度: 时间频度,一个算法执行所耗费的时间。算法中通常用数据比较次数与数据移动次数 进行衡量。空间复杂度: 算法执行所需要的内存大小。


冒泡排序(Bubble Sort)

1.基本思想

冒泡排序是一种交换排序,核心是冒泡,把数组中最小的那个往上冒,冒的过程就是和他相邻的元素交换。

重复走访要排序的数列,通过两两比较相邻记录的排序码。排序过程中每次从后往前冒一个最小值,且每次能确定一个数在序列中的最终位置。若发生逆序,则交换;有俩种方式进行冒泡,一种是先把小的冒泡到前边去,另一种是把大的元素冒泡到后边。

趣味解释:有一群泡泡,其中一个泡泡跑到一个泡小妹说,小妹小妹你过来咱俩比比谁大,小妹说哇你好大,于是他跑到了泡小妹前面,又跟前面的一个泡大哥说,大哥,咱俩比比谁大呗。泡大哥看了他一眼他就老实了。这就是内层的for,那个泡泡跟每个人都比一次。

话说那个泡泡刚老实下来,另一个泡泡又开始跟别人比谁大了,这就是外层的for,每个泡泡都会做一次跟其他泡泡比个没完的事。

2.实现逻辑

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

  • 针对所有的元素重复以上的步骤,除了最后一个。

  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

通过两层循环控制:

  • 第一个循环(外循环),负责把需要冒泡的那个数字排除在外;

  • 第二个循环(内循环),负责两两比较交换。

3.动图演示bubble_sort

4.性能分析

  • 平均时间复杂度:O(N^2)

  • 最佳时间复杂度:O(N)

  • 最差时间复杂度:O(N^2)

  • 空间复杂度:O(1)

  • 排序方式:In-place

  • 稳定性:稳定

解析说明:冒泡排序涉及相邻两两数据的比较,故需要嵌套两层 for 循环来控制。外层循环 n 次,内层最多时循环 n – 1次、最少循环 0 次,平均循环(n-1)/2;所以循环体内总的比较交换次数为:n*(n-1) / 2 = (n^2-n)/2 按照计算时间复杂度的规则,去掉常数、去掉最高项系数,其复杂度为O(N^2) 最优的空间复杂度为开始元素已排序,则空间复杂度为 0;最差的空间复杂度为开始元素为逆排序,则空间复杂度为 O(N); 平均的空间复杂度为O(1)

注:

  • n:数据规模

  • k:”桶”的个数

  • In-place:占用常数内存,不占用额外内存

  • Out-place:占用额外内存

5.代码实现

 
   
   
 
  1. // 冒泡排序(C++版)

  2. void bubble_sort(int arr[], int len)

  3. {

  4. int i, j;

  5. for (i = 0; i < len; i++)

  6. for (j = 1; j < len - i; j++)

  7. if (arr[j - 1] > arr[j])

  8. swap(arr[j - 1], arr[j]);

  9. }

6. 优化改进

6.1 改进方法①

场景一:在某次遍历中如果没有数据交换,说明整个数组已经有序。若初始序列就是排序好的,如果用基础的冒泡排序方法,仍然还要比较O(N^2)次,但无交换次数。

改进思路:通过设置标志位来记录此次遍历有无数据交换,进而可以判断是否要继续循环,设置一个flag标记,当在一趟序列中没有发生交换,则该序列已排序好,但优化后排序的时间复杂度没有发生量级的改变。

改进代码:

 
   
   
 
  1. // 冒泡排序改进(C++版)

  2. void bubble_sort(int arr[], int len)

  3. {

  4. for (int i = 0; i < len-1; i++){ //比较n-1次

  5. bool exchange = true; //冒泡的改进,若在一趟中没有发生逆序,则该序列已有序

  6. for (int j = len-1; j >i; j--){ //每次从后边冒出一个最小值

  7. if (arr[j] < arr[j - 1]){ //发生逆序,则交换

  8. swap(arr[j], arr[j - 1]);

  9. exchange = false;

  10. }

  11. }

  12. if (exchange){

  13. return;

  14. }

  15. }

  16. }

6.2 改进方法②

场景二:如果有100个数的数组,仅前面10个无序,后面90个都已排好序且都大于前面10个数字,那么在第一趟遍历后,最后发生交换的位置必定小于10,且这个位置之后的数据必定已经有序了。

改进思路:记录某次遍历时最后发生数据交换的位置pos,这个位置之后的数据显然已经有序了。因此通过记录最后发生数据交换的位置就可以确定下次循环的范围了。由于pos位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到pos位置即可。

改进代码:

 
   
   
 
  1. // 冒泡排序改进(C++版)

  2. void bubble_sort(int arr[], int len)

  3. {

  4. int j, k;

  5. int flag;

  6. flag = len;

  7. while (flag > 0)

  8. {

  9. k = flag;

  10. flag = 0;

  11. for (j = 1; j < k; j++)

  12. if (arr[j - 1] > arr[j])

  13. {

  14. swap(arr[j - 1], arr[j]);

  15. flag = j;

  16. }

  17. }

  18. }


总结

冒泡排序毕竟是一种效率低下的排序方法,在数据规模很小时,可以采用。数据规模比较大时,建议采用其它排序方法。其他相关排序算法会在后续系列中逐一来分析说明,敬请关注!