算法1.2.冒泡排序

一、什么是冒泡排序

冒泡排序(Bubble Sort),是一种计算机科学领域最基础且较简单的排序算法,是新手入门学习算法的必经之路。

冒泡排序的思路非常简单,就是不断选出数组中的最大值或最小值,每选出一个数,参与选举数组长度减一。

从下图中可以形象的看到,越大的数字代表越大的泡泡,从而最先往上浮,泡泡最终根据大小先后浮出水面。
请添加图片描述

二、冒泡排序的实现
1 基本实现思路

冒泡排序有几种简单的实现方式,我在这里将以从小到大排列数组为例展示冒泡算法。

假定有一数组

5 , 2 , 4 , 6 , 1 , 3 {5,2,4,6,1,3} 5,2,4,6,1,3

我们要如何将其进行从小到大排列呢?

  1. 首先,我们找到这个数组中的最大值,也就是6,让它与最后的数字3交换,得到:

    5 , 2 , 4 , 3 , 1 , 6 {5,2,4,3,1,6} 5,2,4,3,1,6​​

  2. 此时作为最大数的6已经就位,我们关注的数组范围减少一位,也就是说我们将不再关注6。

    5 , 2 , 4 , 3 , 1   { 6 } {5,2,4,3,1}\ \{6\} 5,2,4,3,1 {6}​​​

  3. 我们从剩下的数字中寻找最大的数,也就是5,将其和倒数第二位的数进行交换,得到:
    1 , 2 , 4 , 3   { 5 , 6 } {1,2,4,3}\ \{5,6\} 1,2,4,3 {5,6}

  4. 接下来则按照这个规则继续,最后得到
    1 , 2 , 3 , 4 , 5 , 6 {1,2,3,4,5,6} 1,2,3,4,5,6

是不是非常简单!Ψ( ̄∀ ̄)Ψ

2 求最大值

在上述描述中我隐藏了提取最大值的方式,这里有两种方式来获取最大值。

  1. 在最基础的冒泡排序算法里,我们一般通过不断对比相邻数字,从而原址筛选出最大值,如针对

    { 5 , 2 } , 4 , 6 , 1 , 3 \{5,2\},4,6,1,3 {5,2},4,6,1,3​​

    首先对比5和2,5大于2,交换两者,得到

    2 , { 5 , 4 } , 6 , 1 , 3 2,\{5,4\},6,1,3 2,{5,4},6,1,3​​

    继续对比5和4,5大于4,交换两者,得到

    2 , 4 , { 5 , 6 } , 1 , 3 {2,4,\{5,6\},1,3} 2,4,{5,6},1,3

    继续对比5和6,5小于6,保持不变,得到

    2 , 4 , 5 , { 6 , 1 } , 3 {2,4,5,\{6,1\},3} 2,4,5,{6,1},3

    对比6和1,1小于6,交换两者,得到

    2 , 4 , 5 , 1 , { 6 , 3 } {2,4,5,1,\{6,3\}} 2,4,5,1,{6,3}

    最后对比6和3,3小于6,交换两者,得到

    2 , 4 , 5 , 1 , 3 , { 6 } 2,4,5,1,3,\{6\} 2,4,5,1,3,{6}

    这种方法虽然找出了最大值,但是一般来说需要多次交换数组中的元素,造成了不必要的时间浪费,毕竟我们的目的只是为了获得最大值,因此,有了第二种方法。

  2. 在第二种方法中需要一个额外的变量,我们把它命名为 max。

    首先我们使其记录数组中最后一个数字的下标(数组长度减一)。

    随后,将数组中数字与max所记录下标表示数字进行对比,若数字大于max所记录下标表示数字,则让max记录新的数字的下标。

    最后,交换max记录下标数字与最后的数字。

    5 , 2 , 4 , 6 , 1 , 3 {5,2,4,6,1,3} 5,2,4,6,1,3

    在这个数组中的第一轮,我们让max记录5也就是代表3;

    拿3分别和 5 2 4 6 1 进行比较,前面5 大于3,因此max记录0,随后2、4小于3,max不变,6大于4,max变为3,后续1小于6,最后max为3,也就是代表6;

    最后交换第4位数字与最后1位数字(第6位),获得 5 , 2 , 4 , 3 , 1 , { 6 } 5,2,4,3,1,\{6\} 5,2,4,3,1,{6}​​。​

    这种方法避免了过多的交换,节省了一点时间。

3 伪代码实现

下面我们通过伪代码进行实现

第一种方式进行最大值提取的代码

get arr[] and arr.length //我们获取一个数组 arr ,并且得到它的长度 arr.length

// 第一层循环,表示参与寻找最大值的数组,开始为整个数组(最后元素的下标为arrlength-1),最后只剩一个元素时不需要寻找最大值
for i = (arrlength-1) to 1 
	//第二层循环,不断对比相邻数字(对比j与j-1)
	for j = 1 to i
		if arr[j]<arr[j-1]
			exchange arr[j-1] and arr[j]// 交换j和j-1
		endif
	endfor
endfor

第二种方式进行最大值提取的代码

get arr[] and arr.length //我们获取一个数组 arr ,并且得到它的长度 arr.length

// 第一层循环,表示参与寻找最大值的数组,开始为整个数组(最后元素的下标为arrlength-1),最后只剩一个元素时不需要寻找最大值
for i = (arrlength-1) to 1 
	max = i // max记录最大值所要放的元素下标
	//第二层循环,对比max代表的数字与参与寻找最大值的数字
	for j = i-1 to 0 
		if arr[j]>arr[max]
			max = j
		endif
	endfor
	exchange arr[max] and arr[i] // 交换最后一个数字和最大值
endfor

是不是非常简洁和简单!这里只给出了伪代码的原因是,伪代码的可读性高于真实的代码,我也会给出一份c++实现的代码,关注公众号 愿人人如龙 回复冒泡排序即可免费领取(代码非常简陋,仅供学习使用)。

到了这里,冒泡排序就完了吗?不不不,我们再来看一种特殊情况

1 , 2 , 3 , 4 , 5 , 6 {1,2,3,4,5,6} 1,2,3,4,5,6

1 , 2 , 3 , 4 , 6 , 5 {1,2,3,4,6,5} 1,2,3,4,6,5

1 , 2 , 4 , 3 , 6 , 5 {1,2,4,3,6,5} 1,2,4,3,6,5

对,就是完全排完序或前面部分排序时。

当我们把后面部分排序完毕后,前面的排序无需继续,因为那只是白白浪费时间,那问题是如何去检测这种情况呢?

这对于第一种方式进行最大值提取来说非常简单,只需要增加一条判断。

get arr[] and arr.length 

for i = (arrlength-1) to 1 
	flag = true // 增加一个判断的标志	《------新增
	for j = 1 to i 
		if arr[j]<arr[j-1]
			exchange arr[j-1] and arr[j]
			flag = false // 如果发生了数字的交换则把flag置为false	《------新增
		endif
	endfor
	// 如果一次交换都没发生,代表前面部分已经处于排好序的状态	《------新增
	if flag
		break;
	endif	
endfor

对于这种方式来说,检测这种情况非常简单,但对于第二种来说需要额外牺牲一些时间来到达目的,这是否值得也是不确定的,这里就不展开了,我们要做的是熟悉算法的思路与特点,最后判断使用何种算法,算法在大部分时间不分好和坏,只分合不合适。

quiz:这里我们没有讨论如果数组中有数字是相等的会发生什么,会影响我们的排序吗?(答案是不会)对于不太清楚的童鞋可以尝试一下并思考一下,为什么?

三、冒泡排序算法的分析

关于算法的分析,我们主要讨论时间与空间复杂度。

1 空间复杂度

一般来说,空间复杂度不太重要,我们只需要了解其大致需要哪些空间就行。

空间复杂度就是在排序过程中临时变量所占的内存空间,在这个过程中交换数字需要临时变量,max、flag 均算临时变量,他们相比于数组的输入规模 n n n​​ 来说是常量,不随 n n n 的变化而变化。

在空间复杂度的分析中,有一个问题,可以重复利用的变量空间,需要对其进行空间复杂度计算吗,例如对于交换数字需要临时变量,每一次大循环,需要有一个临时变量,但是这个临时变量可以重复利用,如果考虑 n n n​​ 次循环,每次循环都进行数字交换,不考虑重复利用则需要 Θ ( n ) \Theta(n) Θ(n)​​ 个空间,考虑重复利用则只需要 Θ ( 1 ) \Theta(1) Θ(1)​​​​​​ ,一般来说,我们对于不同循环的还是重复计算,主要是虽然在实际中,诸如单个变量我们可以确保重复使用,但并不是所有情况,临时变量都可以确保重复使用。

因此,上述几种不同的冒泡排序实现在不同情况下,有不同的空间复杂度,但是详尽的去分析意义不大。

但是,对于冒泡排序,我们只需要知道其只需要常数个临时空间就够了,并且这些空间可以重复使用。

2 时间复杂度

之前我们提到时间复杂度主要由算法时间函数的增长量级决定,在这里我们假定数据计算、变量定义赋值、交换、判断的时间均为1,在更精细的分析中应设定其抽象的运行时间 a i a_i ai​ 来定义不同的操作。

因此,对于冒泡排序,我们主要是分析其双层循环部分。

第一个版本

对于第一个版本(按上面伪代码顺序),对于 n n n​ 个数

第一层循环第一次循环内部需要

对比相邻数字 n − 1 n-1 n1​ 次,交换元素 a 1 ( n − 1 ) a_1(n-1) a1(n1)次,其中 0 ≤ a 1 ≤ 1 0\le a_1\le 1 0a11

第一层循环第而次循环需要

对比相邻数字 n − 2 n-2 n2​​​ 次,交换元素 a 2 ( n − 2 ) a_2(n-2) a2(n2)​​​次,其中 0 ≤ a 2 ≤ 1 0\le a_2\le 1 0a21​​​

总时间为: ( n − 1 ) + ( n − 2 ) + . . . + 1 + ( a 1 ( n − 1 ) + a 2 ( n − 2 ) + . . . + a n − 1 ) (n-1)+(n-2)+...+1+(a_1(n-1)+a_2(n-2)+...+a_{n-1}) (n1)+(n2)+...+1+(a1(n1)+a2(n2)+...+an1)​​

考虑最好的情况,即 a 1 . . . a n − 1 a_1...a_{n-1} a1...an1 均为0,当数组按顺序排好时成立:

根据等差数列求和,得到 n ( n − 1 ) 2 \displaystyle\frac{n(n-1)}{2} 2n(n1)

考虑最差情况,即 a 1 . . . a n − 1 a_1...a_{n-1} a1...an1 均为1,当数组按逆序排好时成立:

那时间为 n ( n − 1 ) n(n-1) n(n1)

最好最差的时间复杂度均为 Θ ( n 2 ) \Theta(n^2) Θ(n2) ,那根据夹逼准则,最好<平均情况<最坏,因此也是 Θ ( n 2 ) \Theta(n^2) Θ(n2)

第二个版本
第二个版本

第一层循环第一次循环内部需要:

定义max需要1次,对比数字 n − 1 n-1 n1​​​​ 次,交换数字1次,共 n + 1 n+1 n+1​ 次。

后续每一层循环减1,最后总时间构成一个等差数列,时间复杂度为 Θ ( n 2 ) \Theta(n^2) Θ(n2)​​​​ ,这是最差情况。

最好情况在于不需要交换数字,所以 Θ ( n 2 − n ) \Theta(n^2-n) Θ(n2n)​ , 时间复杂度同样为 Θ ( n 2 ) \Theta(n^2) Θ(n2)​。

同理,根据夹逼准则,最好<平均情况<最坏,因此也是 Θ ( n 2 ) \Theta(n^2) Θ(n2)

第三个版本

最坏情况下相比第一个版本多了 Θ ( n ) \Theta(n) Θ(n) 次关于flag的操作,因此最差情况时间复杂度为 Θ ( n 2 ) \Theta(n^2) Θ(n2)​ 。

最好情况为已经按顺序排好,需要 n − 1 n-1 n1​ 次对比,加1次flag定义和1次flag赋值,因此最好情况时间复杂度为 Θ ( n ) \Theta(n) Θ(n)​ 。

在这里最差的情况和最好的情况不一致,因此,不能像上面两个版本一样直接得到平均情况时间复杂度,需要一点概率论的知识,分析过程比较复杂,这里就不对其进行分析,其平均运行时间也同样为 Θ ( n 2 ) \Theta(n^2) Θ(n2)。​

四、结语

排序算法作为一个经典的算法,虽然其不是一个非常好的算法,但是其中蕴含的思想非常适合初学者,是每个学习算法的人必学的排序算法,下一节将讲解插入排序算法。关注公众号 愿人人如龙 回复冒泡排序即可免费领取(代码非常简陋,仅供学习使用)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值