【算法】小规模排序

在将排序之前,首先思考一个问题:选择排序、插入排序、冒泡排序的时间复杂度均为 o(n) ,为什么大家在讲排序的时候首先更愿意讲插入排序?

在分析一个排序算法好坏的时候,往往要考虑这么几个方面:

1.时间复杂度

考虑到时间复杂度的时候就要考虑到最好情况和最坏情况,尽可能多的有序当然是最好的情况,完全逆序则是最坏的情况,有序度不同,算法的性能也不同。

2.空间复杂度

算法的内存消耗可以通过空间复杂度来衡量。其中,原地排序算法,指的是空间复杂度 o(1)的算法。

3.排序算法的稳定性

稳定性指的是,如果待排序的序列中存在值相等的元素,经过排序以后,相等元素之间原有的先后顺序不变。

举个例子,如果现在我们要给电商交易系统的“订单”排序,订单有俩个属性,一个是下单时间,一个是订单金额。对于时间相同的订单,我们希望按照金额从小到大排序,对于金额相同的订单,我们希望按照下单时间从早到晚排序。那么我们可以按照俩个属性分别排一次续,如果是稳定的排序算法,假设第一次按时间排序,第二次按照金额排序,那么稳定算法可以保持金额相同的俩个对象,在排序后的前后顺序不变。


首先介绍一下三种排序算法:

冒泡排序

冒泡排序只会操作俩个相邻的数据,每次冒泡操作都会对相邻的俩个元素进行比较,如果不满足大小关系要求就让它俩互换。一次有效的冒泡至少会让一个元素移动到它应该在的位置,重复N次就完成了N个数据的排序工作。

实际上,冒泡还可以再优化。当某趟冒泡操作已经没有数据可以交换的时候,说明已经达到了完全有序,后面的冒泡就可以不需要了。因此,可以在循环中加一个flag,如果每趟有交换的话,就置为true,如果没有就置为false,跳出循环。

def bubble_sort(li:list)->list:
    if len(li) <=1:
        return li
    for i in range(len(li)):
        flag = False
        for j in range(0,len(li)-i-1):
            if li[j] >li[j+1]:
                li[j],li[j+1] = li[j+1],li[j]
                flag =True
        if not flag:
            return li
    return li

1.冒泡排序是一个原地排序算法,只涉及相邻数据的交换工作,只需要常量级的空间。

2.当相邻俩元素相等的时候,我们选择不交换,此时冒泡排序是稳定的排序算法。

3.最好的情况下,排序的数据有序,只要一次冒泡即可结束,最好的时间复杂度是o(n),最坏的情况是倒序排列,我们需要n次冒泡操作,最坏的情况时间复杂度是o(n**2)。


插入排序

插入排序的思想是,有一个有序的数组,当我们往里面添加一个新的数据后,用一种方法将继续保持数据有序: 遍历数组,找到数据数据应该插入的地方插入,把后面的数据都往后挪移位。

插入排序中包含俩种操作,一种是元素的比较,一种是元素的移动。当我们需要将一个个数据插入到已排序区间时,需要与排序区间的元素依次比较大小,找到合适的插入位置。找到插入点以后,我们还需要将插入点之后的元素顺序往后移一位,这样才能腾出位置给插入元素。

def insert_sort(li:list)->list:
    if len(li) <=1:
        return li
    for i in range(1,len(li)):
        temp = li[i]
        j = i
        while j>=1:
            if li[j-1]>temp:
                li[j] = li[j-1]
            else:
                break
            j -=1
        li[j] = temp
    return li

1.插入排序算法的空间复杂度是o(1),是原地排序算法。

2.当对值相同的元素,我们可以选择将后面出现的元素插入到前面元素的后面,及相等的时候不比较,这样就可以保持原有的前后顺序不变,所以插入排序也是稳定的排序算法。

3.当原数据是有序的,我们如果从尾到头遍历一遍数据即可,此时最好时间复杂度为o(n),如果数据是逆序的,每次插入都相当于在数组的第一个位置插入新的数据,最坏时间复杂度为o(n**2)。

插入排序的优化为希尔排序,由于插入排序在对几乎已经排好序的数据操作的时候效率非常高,可以达到线性排序效果,但是每次排序都只能移动一步,所以使用了递减步长的方法先把插入排序变得基本有序,然后当步长为1的时候即为插入排序。但是由于这样前期没有涉及相邻俩个元素的操作,所以希尔排序是一个不稳定的算法。

def shell_sort(li:list) ->list:
    if len(li) <=1:
        return li
    step = len(li)//2
    while step >0:
        for i in range(step,len(li)):
            temp = li[i]
            j = i
            while j >= step:
                if li[j-step]>temp:
                    li[j] = li[j-step]
                else:
                    break
                j -=step
            li[j] = temp
        step = step //2
    return li

为什么选择插入排序

在冒泡排序的时候我使用了Python的语法糖,而实际上冒泡排序的数据交换要比插入排序的移动要复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个:

#冒泡排序
if
li[j]>li[j+1]: temp = li[j] li[j] = li[j+1] li[j+1] = temp

#插入排序
if li[j]>li[j+1]: li[j+1] = li[j] else: break

对于一个逆序度为K的数组进行排序,用冒泡排序,总交换耗时是3*K个单位时间,而插入排序只要K个单位时间。


选择排序

选择排序的实现有点类似插入排序,也分已排序区间和未排序区间,但是不是移动以排好序的元素实现的交换,而是每一次从未排序的区间中找到最小的元素,将其放到已排区间的末尾。

def select_sort(li:list)->list:
    if len(li) <=1:
        return li
    for i in range(len(li)-1):
        min_index = i
        for j in range(i+1,len(li)):
            if li[j]<li[min_index]:
                min_index=j
        li[i],li[min_index] = li[min_index],li[i]
    return li

1.选择排序是一种原地排序算法,其空间复杂度为o(1)。

2.选择排序最好最坏和平均时间复杂度都为o(n**2),因为不管有序还是无序,都要二层循环遍历完。

3.选择排序不是一个稳定的排序算法,因为不涉及相邻元素的操作,以5,8,5,2,9数组为例,第一次找到了2,与第一个5交换顺序,那么俩个5的顺序就变了。

因此不仅执行语句多,时间复杂度没有优势,还不稳定,选择排序相比前俩个排序并没有多大优势。

原文地址:https://www.cnblogs.com/guangluwutu/p/11805494.html