排序算法(一)冒泡排序与两种改进(python)

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)级别的额外空间。
此外,冒泡排序属于稳定的排序,即排序结束后,相等的元素间保持了排序前的先后次序。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值