一看就懂的冒泡排序
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位置前。」
冒泡排序是稳定排序
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,「如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的」;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法
总结
冒泡排序效率是及其低下的, 我们主要是要去学习它的思想, 而在实际工作中,一般很少用如此慢的排序算法。
以上是关于一看就懂的冒泡排序的主要内容,如果未能解决你的问题,请参考以下文章