1 冒泡排序
1.1 执行流程
冒泡排序也叫做起泡排序。以升序为例,其执行流程为:从头开始比较每一对相邻元素,如果第1个比第2个大,就交换它们的位置,执行完一轮之后(遍历完0到n-1的元素),最后一个元素就是整个数组中最大的元素;之后再对0至n-2的元素进行相同操作…直到对最后两个元素作比较(索引为0与1的元素)。
1.2 代码思路
从执行流程可知,每次遍历将确定一个遍历元素中的最大元素,那么让数组达到完全有序,我们只需要进行n-1次的遍历(一共n个元素),而每次遍历元素的范围是不同的,终点索引依次减少,因为我们可以使用第一个for循环确定遍历的终点,在进行比较操作时,我们可以让当前索引为i的元素与i+1的元素比较,所以比较完所有的元素时i应该是n-2(一共n个元素,最后一个元素索引为n-1),至此,for循环中i的范围应该是[n-2,0],这里较大的数字在前表示递减。
for e in range(n-2,-1,-1):#每次冒泡中最后一个元素的索引[n-1,0]
在确定终点后,我们需要对确定范围内的所有元素进行比较,起点必然是0,所以第二个for循环就十分简单了,只需要依次进行比较、交换即可。
def swap(a,b):
return b,a
#冒泡排序原版
def bubble_sort_ori(array):
n=len(array)
for e in range(n-2,-1,-1):#每次冒泡中最后一个元素的索引[n-1,0]
for s in range(0,e+1):#遍历元素[s,e]
if array[s]>array[s+1]:
array[s],array[s+1]=swap(array[s],array[s+1])
1.3 测试正确性
通过numpy模块,我们可以快速地生成确定范围内的随机数组。
#生成5000个1到10000的随机整形数组
array=np.random.randint(0,10000,5000,dtype=int)
我们可以自定义一个检测数组升序的方法,以此来判断排序后数组的有序性,在此若数组并非完全有序,程序将进行 ‘排序失败’ 的打印,当然,我们也可以自定义异常,并在不完全有序时抛出(具体见Python学习笔记(十八):异常处理)。
#检查是否有序
def orderCheck(array):
for i in range(len(array)-1):
if array[i]>array[i+1]:
print('排序失败')
return
print('排序成功')
由于之后还存在着冒泡排序的改进版本,需要调用不同的冒泡排序进行比较,且排序过程中还涉及到消耗时间计算,在此我们使用高阶函数的方法,将函数名作为参数传入自定义的sort方法中进行调用,并在sort方法中统一进行时间消耗的计算。
def sort(sort_algorithm,ori_array):
#先复制一份数组,再进行更改
array = np.copy(ori_array)
start=time.clock()
sort_algorithm(array)
end=time.clock()
total_time=float(end-start)
print(sort_algorithm.__name__+" : %0.5f" % total_time)
orderCheck(array)
下面就可以进行测试了。
array=np.random.randint(0,10000,5000,dtype=int)
sort(bubble_sort_ori,array)
可以看到,排序是成功的。
1.4 冒泡排序改进1
如果数组一开始就是有序的,而使用之前的冒泡排序,将会发生什么?显然,哪怕数组已经有序了,遍历操作依然会进行,不断地去比较,造成极大的浪费。在此我们可以通过一个bool变量来判断整个数组的有序性,在某次遍历中如果交换还在进行,说明数组仍未有序,需要继续循环遍历;若某在遍历中没有发生交换,那么,说明数组已经有序了,在该次遍历结束后,我们需要直接退出。
#冒泡排序改版1,如果发现有序,则不再遍历,直接结束排序
def bubble_sort_ver1(array):
n=len(array)
for e in range(n-2,-1,-1):#每次冒泡中最后一个元素的索引[n-1,0]
ordered=True
for s in range(0,e+1):#遍历元素[s,e]
if array[s]>array[s+1]:
array[s],array[s+1]=swap(array[s],array[s+1])
ordered=False
if ordered:
return
1.4 冒泡排序改进2
如果序列尾部已经局部有序,可以记录最后1次交换的位置,减少比较次数。以下为具体示例。
当我们对一个数组进行第一次遍历排序时(橙色表示未确定最终位置)。
那么这次遍历后,数组的结果为:
可以看到,需要遍历的元素的尾部可能已经存在了有序的元素,我们可以通过最后一次进行交换的位置,来跟新遍历结束的索引,以此来减少遍历遍历与比较的次数。
1.4.1 代码实现细节
我们可以通过整型变量e_new记录最后一次交换时i的位置,并在该次遍历结束后修改外部循环e的大小(end的含义),需要注意的是,如果没有进行交换,那e_new应该为何值,又是否需要对e作修改?如果没有进行交换,则说明数组已经有序了,我们需要对进行修改,直接退出循环,为此我们可以直接赋予e_new初值为-1,并不管是否进行了交换都对e作修改,这样,当数组提前有序,排序也将提前结束。最后,是进行交换时e_new的取值,当最后一次交换的索引为i时(索引为i与i+1的元素进行的交换),无法确定是否有序的元素为[0,i],而我们又是使用当前元素与后一个元素进行的比较,所以下一次的e应该是i-1,但事实上,由于外部for循环在该次循环结束将对e作减减操作,在进行交换时,我们还是需要对e_new赋值i。
#冒泡排序改版2,如果发现序列尾部局部有序,则可以通过最后一次交换的位置来减少次数
def bubble_sort_ver2(array):
n=len(array)
for e in range(n-2,-1,-1):#每次冒泡中最后一个元素的索引[n-1,0]
e_new=-1#默认新的结束索引为-1,当序列有序时可以直接结束循环
for s in range(0,e+1):#遍历元素[s,e]
if array[s]>array[s+1]:
array[s],array[s+1]=swap(array[s],array[s+1])
e_new=s
e=e_new#注意下面还有e--的操作
1.5 三种排序的比较
#生成200个1到1000的随机数组
array=np.random.randint(0,10000,5000,dtype=int)
sort(bubble_sort_ori,array)
sort(bubble_sort_ver1,array)
sort(bubble_sort_ver2,array)
通过对三种排序进行比较,我们会发现一下结果。
我们惊奇地发现,有时优化后的冒泡排序比原版冒泡排序更快,有时却竟然更慢,这是因为生成的数组具有随机性,当数组本身具有一定的顺序,提前到达有序,或是尾部存在较多有序的元素时,优化后的排序显然更快,但倘若这些条件都不存在,数组十分杂乱,优化后的排序反而因为额外的代码操作变得更慢了。
1.6 时间空间复杂度
冒泡排序的最好时间复杂度为O(n),及数组一开始就有序的情况,但这时我们还是需要一次遍历才能得出这个结论并结束排序。最坏与平均时间复杂度为O(n^2)。
而对于空间复杂度,则为O(1),因为我们直接在原数组上进行改动、排序,只申请了O(1)级别的额外空间。
此外,冒泡排序属于稳定的排序,即排序结束后,相等的元素间保持了排序前的先后次序。