一看就懂的冒泡排序

Posted 程序员的进击之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一看就懂的冒泡排序相关的知识,希望对你有一定的参考价值。

一看就懂的冒泡排序

冒泡排序应该是最经典的排序算法了,想起上大学的时候, 老师对冒泡排序就花了一节课重点讲解,面试中也最喜欢让手撸一个冒泡排序。另外据说奥巴马也写过冒泡排序。所以身为程序员的你,还有什么理由不去掌握它呢,要学会各类排序,就从冒泡排序开始吧。

定义

冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

冒泡排序分「从大到小」和「从小到大」两种排序方式。它们的唯一区别就是两个数交换的条件不同,从大到小排序是前面的数比后面的小的时候交换,而从小到大排序是前面的数比后面的数大的时候交换。「在下面我们只讲从小到大的排序方式。」

「冒泡排序的原理:」 从第一个数开始,依次往后比较,如果前面的数比后面的数大就交换,否则不作处理。这就类似烧开水时,壶底的水泡往上冒的过程。

算法原理

  • 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  • 针对所有的元素重复以上的步骤,除了最后一个。
  • 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

图示

如果排列的数组为:[52, 23, 18, 84, 58, 11], 那么整个排序过程将如下示:

文字讲解

现在有一堆乱序的数,比如:[52, 23, 18, 84, 58, 11]

第一轮迭代:从第一个数开始,依次比较相邻的两个数,如果前面一个数比后面一个数大,那么交换位置,直到处理到最后一个数,最后的这个数是最大的。

第二轮迭代:因为最后一个数已经是最大了,现在重复第一轮迭代的操作,但是只处理到倒数第二个数。

第三轮迭代:因为最后一个数已经是最大了,最后第二个数是次大的,现在重复第一轮迭代的操作,但是只处理到倒数第三个数。

第N轮迭代:....

经过交换,最后的结果为:[11, 18, 23, 52, 58, 84],我们可以看到已经排好序了。

因为小的元素会慢慢地浮到顶端,很像碳酸饮料的汽泡,会冒上去,所以这就是冒泡排序取名的来源。

代码示例

经过上面一堆的各种图示和文字讲解,是否已经跃跃欲试了,先不急着往下看, 可以先自己动手去实现一下,然后再对照着看一下。这里我们采用golang语言实现一下(实现语言不重要,关键是思想)。

基础版

「按冒泡的思想,初步具体实现:」 可以用双层循环, 外层用来控制内层循环中最值上浮的位置, 内层用来进行两两比较和交换位置.

实现代码如下示:

package sort

// 冒泡升序排序
func BubbuleSortAsc(array []int) {
 arrayLen := len(array)
 for i := 0; i < arrayLen-1; i++ {
  for j := 0; j < arrayLen-i-1; j++ {
   if array[j] > array[j+1] {
    array[j], array[j+1] = array[j+1], array[j]
   }
  }
 }
}

但我们会发现针对一些情况,我们的算法还可以做进一步的优化。

改进思想1: 处理在排序过程中数组整体已经有序的情况

假设我们有一组数据:[1, 2, 3, 5, 4, 6] 经过一轮冒泡数组已经变为:[1, 2, 3, 4, 5, 6], 此时数组已经是有序的了,但是按照我们上面的算法,此时我们还需要继续进行往下一轮一轮的比对,虽然这个时候只有比较操作而没有交换操作, 但这些比较操作仍然是没有必要的.

「利用上面的原理, 可以对经典实现进行改进」: 里面一层循环在某次扫描中没有执行交换,则说明此时数组已经全部有序列,无需再扫描了。设置一个标记位来标记此次遍历是否发生了交换,如果没有发生交换说明已经完成排序;

改版代码如下示:

package sort

// 冒泡升序排序
func BubbuleSortAsc(array []int) {
 arrayLen := len(array)
    // 设置标志位
 endFlag := true
 for i := 0; i < arrayLen-1; i++ {
  endFlag = true
  for j := 0; j < arrayLen-i-1; j++ {
   if array[j] > array[j+1] {
    array[j], array[j+1] = array[j+1], array[j]
    endFlag = false
   }
  }
  if endFlag {
   break
  }
 }
}

改进思想2: 数组局部有序

若数组是局部有序的, 例如从某个位置开始之后的数组已经有序, 则没有必要对这一部分数组进行比较了.

或者直观的描述就是: 如果array[i:len-1]已是有序区间,需要扫描区间是array[0:i],记上次扫描时最后 一次执行交换的位置为lastSwap,则lastSwap在0与i之间,则array[lastSwapPos:i]区间也是有序的,否则这个区间也会发生交换;所以下次扫描区间就可以由array[0:i] 缩减到[0:lastSwap]。

「此时的改进方法是:」 在遍历过程中可以记下最后一次发生交换事件的位置, 下次的内层循环就到这个位置终止, 可以节约多余的比较操作.

使用一个变量来保存最后一个发生了交换操作的位置, 并设置为下一轮内层循环的终止位置:

实现代码如下示:

package sort

// 冒泡升序排序
func BubbuleSortAsc(array []int) {
 arrayLen := len(array)
 lastSwap := arrayLen - 1
 lastSwapTemp := arrayLen - 1
 for i := 0; i < arrayLen-1; i++ {
  lastSwap = lastSwapTemp
  for j := 0; j < lastSwap; j++ {
   if array[j] > array[j+1] {
    array[j], array[j+1] = array[j+1], array[j]
    lastSwapTemp = j
   }
  }
  if lastSwap == lastSwapTemp {
   break
  }
 }
}

思想1和思想2结合

将思想1和2结合起来, 处理数组局部有序和排序过程中整体有序的情况,代码也很容易实现, 如下示:

package main

// 冒泡升序排序
func BubbuleSortAsc(array []int) {
 arrayLen := len(array)
 lastSwap := arrayLen - 1
 lastSwapTemp := arrayLen - 1

 endFlag := true
 for i := 0; i < arrayLen-1; i++ {
  lastSwap = lastSwapTemp
  for j := 0; j < lastSwap; j++ {
   if array[j] > array[j+1] {
    array[j], array[j+1] = array[j+1], array[j]
    lastSwapTemp = j
    endFlag = false
   }
  }
  if lastSwap == lastSwapTemp {
   break
  }
  if endFlag {
   break
  }
 }
}

复杂度

  • 时间复杂度分析:
其外层循环执行 N - 1次。内层循环最多的时候执行N次,最少的时候执行1次,平均执行 (N+1)/2次。
所以循环体内的比较交换约执行 (N - 1)(N + 1) / 2 = (N^2 - 1)/2(其中N^2是仿照Latex中的记法,表示N的平方)。按照计算复杂度的原则,去掉常数,去掉最高项系数,其复杂度为O(N^2)。

按照改进的算法,对于一个已经有序的数组,算法完成第一次外层循环后就会返回。
实际上只发生了 N - 1次比较,所以最好的情况下,该算法复杂度是O(N)。

稳定排序

什么是稳定排序

通俗地讲就是能保证排序前「2个相等的数」其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。在简单形式化一下,「如果Ai = Aj,Ai原来在位置前,排序后Ai还是要在Aj位置前。」

冒泡排序是稳定排序

冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,「如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的」;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法

总结

冒泡排序效率是及其低下的, 我们主要是要去学习它的思想, 而在实际工作中,一般很少用如此慢的排序算法。


以上是关于一看就懂的冒泡排序的主要内容,如果未能解决你的问题,请参考以下文章

一看就懂的快速排序

算法一看就懂之「 冒泡排序 」

一看就懂的大数据排序算法:如何给100万用户数据排序?

一看就懂的大数据排序算法:如何给100万用户数据排序?

算法一看就懂之「 插入排序 」

『荐读』算法一看就懂之「 插入排序 」