排序算法原理详解,结合python实现

1、冒泡排序

冒泡排序算法的原理很容易理解,对n个元素来说,每次从第1个元素开始遍历直到第n-1个元素,每走一步将其与其后一个元素比较,如果大于就替换,否则保持不变(故是稳定的);这样遍历一次之后,就将最大的元素移到末尾。第二次遍历排除上一步移到末尾的元素,即将余下的n-1个元素重复之前的操作,将最大的元素移到末尾;第三次遍历排除上一步移到末尾的元素,将余下的n-2个元素重读之前的操作;再是n-3、n-4...2,就像冒泡一样每遍历一次都将最大的元素往后放,故名为冒泡排序。

python实现

def bubble_sort(alist):
    # i取值为n-1,n-2,n-3,...,1
    for i in range(len(alist)-1,0,-1):
        # j取值为n-1,n-2,n-3,...,1
        for j in range(i):
            if alist[j] > alist[j+1]:
                alist[j],alist[j+1] = alist[j+1],alist[j]
    return alist

ll = [2,3,1,5,6,1,6,7]
print(bubble_sort(ll))

综上所述,冒泡排序是稳定的,两层循环,故时间复杂度为O(n2)

 

2、插入排序

插入排序算法的思想就是将元素分为两个部分,将元素拆分为第一个元素是有序部分,剩下的n-1个元素是无序部分,在无序序列中依次取得值插入到有序序列,将插入进来的元素与有序序列的各个元素进行比较,使有序的部分还是有序的。

步骤详解

原始序列

78  34  45  2  5  70  43  2  55

拆成两部分,前面是有序,右面是无序

[78]                [34  45  2  5  70  43  2  55]

 

取出34放入有序序列

[78  34]              [45  2  5  70  43  2  55]

将78与34比较,78>34,故两元素替换

[34  78]                 [45  2  5  70  43  2  55]

取出45放入有序序列

[34  78  45]                 [2  5  70  43  2  55]

将45从末尾开始依次与有序序列元素比较,78>45,34<45,故变为

[34  45  78]                 [2  5  70  43  2  55]

取出2放入有序序列

[34  45  78  2]                 [5  70  43  2  55]

2<78,2<45,2<34,故变为

[2  34  45  78]                 [5  70  43  2  55]

同理,取出5,得到

[2  5  34  45  78]             [70  43  2  55]

取出70,得到

[2  5  34  45  70  78]                [43  2  55]

取出43,得到

[2  5  34  43  45  70  78]             [2  55]

取出2,有

[2  2  5  34  43  45  70  78]             [55]

取出55,有

[2  2  5  34  43  45  55  70  78]

python实现

def insert_sort(alist):
    # i取值 1,2,3,...,n-1
    for i in range(1,len(alist)):
        # j取值 [1],[2,1],[3,2,1],...,[n-1,n-2,...,1]
        for j in range(i,0,-1):
            if alist[j-1] > alist[j]:
                alist[j-1],alist[j] = alist[j],alist[j-1]
    return alist

ll = [78,34,45,2,5,70,43,2,55]
print(insert_sort(ll))

插入排序按照顺序从无序的序列中取得数据,将数据与有序序列中的元素进行比较,如果此元素小则交换位置,否则不交换,不包含等于的情况,故插入排序是稳定的,两层循环遍历元素,时间复杂度为O(n2)

 

3、选择排序

选择排序与插入排序的思想类似,每遍历一次元素,选择元素中最小的值与首位元素替换(前半部分有序,后半部分无序),再将余下的n-1个元素重复此操作,直至元素为2个(2个元素以上才可以选择出最小)为止。

步骤详解

原始序列(n个元素)

54  226  93  17  77  31  44  55  28

第1次遍历n个元素,选择最小的元素与首位元素替换,变为

17             226  93  54  77  31  44  55  28

第2次遍历n-1个元素,选择最小的元素与首位元素替换,变为

17  28            93  54 77  31  44  55  226

第3次遍历n-2个元素,选择最小的元素与首位元素替换,变为

17  28   31             54  77  93  44  55  226

第4次遍历n-3个元素,选择最小的元素与首位元素替换,变为

17  28  31  44            77  93  54  55  226

第5次遍历n-4个元素,选择最小的元素与首位元素替换,变为

17  28  31  44   54           93  77  55  226

第6次遍历n-5个元素,选择最小的元素与首位元素替换,变为

17  28  31  44   54   55               77  93  226

第7次遍历n-6(3个)个元素,选择最小的元素与首位元素替换,变为

17  28  31  44   54   55  77             93  226

第8次遍历n-7(2个)个元素,选择最小的元素与首位元素替换,变为

17  28  31  44   54   55  77   93  226

python实现

def select_sort(alist):
    # i为0,1,2,...,n-2
    for i in range(len(alist)-1):
        min_index = i
        # j为[1,2,3,...,n-1],在此下标中选取最小元素
        #    [2,3,4,...,n-1].在此下标中选取最小元素
        #    [3,4,5,...,n-1],在此下标中选取最小元素
        #     ...
        for j in range(i+1,len(alist)):
            if alist[min_index] > alist[j]:
                min_index = j
        alist[i],alist[min_index] = alist[min_index],alist[i]
    return alist

ll = [54,226,93,17,77,31,44,55,28]
print(select_sort(ll))

如数组[22,3,22,1],第一次遍历交换22和1的位置,变为[1,3,22,22],原来的22的顺序就被打乱了

故选择排序是不稳定的,时间复杂度为O(n2)

 

4、快速排序

快速排序目的是找到起始元素在排序过后的“正确”位置,将原数组拆分为左右两部分,左部分的元素始终比此元素小,右部分始终比此元素大,将起始元素放在两拆分元素中间;接着对剩余两部分的元素继续拆分和操作,被拆分成元素个数为1为止,元素最终排序完成。

比如

54  56  92  17  77  31  44  55  28

首先定义一个mid_value指向起始元素位置,定义两游标左右移动,high游标在元素最右边,low元素在元素最左边将元素分为左右两部分,左边比此元素小,右边比此元素大。

判断起始为mid_value,high游标所指向元素末尾,low指向元素起始,当high所指的元素比mid_value大,则high向左移一步,即high -= 1,否则low指向high所指向的元素,既有

判断low所指的元素是否比mid_value小,是则low向右移动,即low += 1,否则与high指向low所指向的元素,既有

比较high,则有

low,有

high,

low,

high,

当high与low重合时,即low = high,将mid_value的值赋予low或high,第一步拆分完成,即找到54所在的正确位置,左边元素都比54小,右边元素都比54大

第二次拆分,以左半部分为例

首先判断high,high指向元素小于mid_value,low指向high所指元素,有

判断low,low指向元素小于mid_value,向右移动,low += 1,变为

low大于mid_value,将high指向low指向的元素,

high指向元素大于mid_value,左移动,

28找到了正确位置,

继续拆分17(左半部分),31和44(右半部分),直至元素个数为1,排序完成。

python实现

def quick_sort(alist,first,last):
    if first >= last:
        return
    mid_value = alist[first]
    low = first
    high = last

    while low < high:
        # high左移
        while low < high and alist[high] >= mid_value:
            high -= 1
        alist[low] = alist[high]

        # low右移
        while low < high and alist[low] < mid_value:
            low += 1
        alist[high] = alist[low]

    # 循环退出时,low == high
    alist[low] = mid_value

    # 递归
    # 对low左边的列表执行快速排序
    quick_sort(alist,first,low-1)
    # 对low右边的列表排序
    quick_sort(alist,low+1,last)

ll = [54,56,92,17,77,31,44,55,28]
print(ll)
# 下标从0开始,len(ll)-1结束
quick_sort(ll,0,len(ll)-1)
print(ll)

代码中,每次判断只有low <= mid_value 或者high >= mid_value时,才能进行快排,或者说,必须有一个游标囊括了=后才有“左小右大”的产生,当数组为  [11(1)    22    33    11(2)] ,判断条件为low <= mid_value时,此时11(2)被分到了左半部分,于是数组变为[22   11(2)   11(1)   33 ],原来的11的顺序就被打乱了,故快速排序不稳定。

在快速排序中,每次找到中间的“正确”位置需要遍历一遍元素若正确位置恰好在中间位置,将n个元素拆分为元素个数为1时,需要进行logn次拆分,故快速排序最优时间复杂度为O(nlogn)。特殊情况是如果排序一个有序数组,如[1,2,3,4,5],需要拆分n次,并比较n次,故快速排序最坏的时间复杂度为O(n2)

 

5、归并排序

归并排序是将元素拆分,直至元素个数为1为止,再将拆分过的元素进行从小到大归并的过程。

            54  56  92  17  77  31  44  55                                原始数组,8

     54  56  92  17              77  31  44  55                           拆分为2部分,4+4

   54  56        92  17        77  31       44  55                        拆分为4部分,2+2+2+2

54      56      92      17      77     31      44      55                 拆分为8部分,1+1+1+1+1+1+1+1

   54  56        17  92         31  77       44  55                       归并为4部分,2+2+2+2(将左右相邻的2元素,小在前大在后)

     17  54  56  92               31  44  55  77                          归并为2部分,4+4(左右游标,小在前大在后)

           17  31  44  54  55  56  77  92                                 继续归并,8,排序好的数组(左右游标,小在前大在后)

归并过程:

python实现

def merge_sort(alist):
    n = len(alist)
    if n <= 1:
        return alist
    mid = n // 2
    # 采用归并排序后形成的有序的新的列表
    left_li = merge_sort(alist[:mid])
    # 采用归并排序后形成的有序的新的列表
    right_li = merge_sort(alist[mid:])
    # 将两个有序的子序列合并为一个新的整体
    # merge(left,right)
    left_pointer, right_pointer = 0,0
    result = []
    while left_pointer < len(left_li) and right_pointer < len(right_li):
        if left_li[left_pointer] <= right_li[right_pointer]:
            result.append(left_li[left_pointer])
            left_pointer += 1
        else:
            result.append(right_li[right_pointer])
            right_pointer += 1
    result += left_li[left_pointer:]
    result += right_li[right_pointer:]
    return result

ll = [17,31,44,54,55,56,77,92]
res = merge_sort(ll)
print(res)

归并排序与快速排序有一定的相似处,都是对元素二分的思想,故节约了时间复杂度,归并排序时间复杂度均为O(nlogn),是稳定的。归并排序不是对原始数据进行排序的,而是把排序后的元素额外申请了空间去存储,故虽归并排序在节约了时间,但增加了其空间复杂度,如对2GB的数据进行排序的时候,机器只有2GB的内存供使用,归并排序需要4GB的内存才能使其排序,此时归并排序就派不上用场了虽然快速排序不稳定,但在实际操作过程中,根据排序的需求,快速排序用到的频率还是较高的。

 

6、希尔排序

希尔排序是插入排序的高效改进版,希尔排序的思想是取得一个间隔gap,将数据拆分成几个部分,再对每个部分进行插入排序,再对处理好的数组再实行gap拆分和插入排序,直至gap取到1为止。

对于原始数组 55 26 93 17 77 31 44 55 20 来说,第一次拆分合并间隔gap为4,有

第二次间隔gap为2

第三次间隔gap为1

python实现

def shell_sort(alist):
    n = len(alist)
    gap = n // 2
    while gap > 0:
        for j in range(gap,n):
            i = j
            while i > 0:
                if alist[i] < alist[i-gap]:
                    alist[i],alist[i-gap] = alist[i-gap],alist[i]
                    i -= gap
                else:
                    break
        gap //= 2

ll = [54,26,93,17,77,31,44,55,20]
shell_sort(ll)
print(ll)

希尔排序是不稳定的,时间复杂度平均情况O(nlogn) ~ O(n2);根据统计算出来的最好情况下时间复杂度为O(n1.3)

 

7、堆排序

理解对排序之前,先要理解什么是堆

1.堆是一种完全二叉树(除了最后一层,其他每层都被完全填充,且保证最后一层节点都向左对齐)

2.大根堆:所有父节点都不小于其子节点

3.小根堆:所有父节点都不大于其子节点

堆排序的思想就是:对给定的数组,每次构造大根堆,将首位(也就是最大)元素与末尾元素互换,再接着构造最大堆,再互换......,直至堆元素的个数为1为止。

给定一个数组  55  77  22  33   45

堆形式为

       

构造大根堆(每次找到最大元素与其父节点交换)

 

将构造的大根堆首位元素(77)与末尾元素(45)互换

序列变为  45  55  22  33  77

排除末尾元素(77)

构造大根堆

将首位元素与末尾元素互换

序列变为 33  45  22  55  77

排除末尾元素

构造大根堆

首位末尾交换

序列变为 22  33  45  55  77(此时已然有序,但为了更能理解堆排序的过程,继续给出图例说明)

排除末尾元素

构造大根堆

首末互换

排除末尾,此时只剩一个元素

序列变为 22  33  45  55  77

此时堆排序已完成

python实现

def swap(a, b):  # 将a,b交换
    temp = a
    a = b
    b = temp
    return a,b

def sift_down(array, start, end):
    """
    调整成大顶堆,初始堆时,从下往上;交换堆顶与堆尾后,从上往下调整
    :param array: 列表的引用
    :param start: 父结点
    :param end: 结束的下标
    :return: 无
    """
    while True:

        # 当列表第一个是以下标0开始,结点下标为i,左孩子则为2*i+1,右孩子下标则为2*i+2;
        # 若下标以1开始,左孩子则为2*i,右孩子则为2*i+1
        left_child = 2*start + 1  # 左孩子的结点下标
        # 当结点的右孩子存在,且大于结点的左孩子时
        if left_child > end:
            break

        if left_child+1 <= end and array[left_child+1] > array[left_child]:
            left_child += 1
        if array[left_child] > array[start]:  # 当左右孩子的最大值大于父结点时,则交换
            array[left_child], array[start] = swap(array[left_child], array[start])

            start = left_child  # 交换之后以交换子结点为根的堆可能不是大顶堆,需重新调整
        else:  # 若父结点大于左右孩子,则退出循环
            break

        print(">>", array)


def heap_sort(array):  # 堆排序
    # 先初始化大顶堆
    first = len(array)//2 -1  # 最后一个有孩子的节点(//表示取整的意思)
    # 第一个结点的下标为0,很多博客&课本教材是从下标1开始,无所谓吧,你随意
    for i in range(first, -1, -1):  # 从最后一个有孩子的节点开始往上调整
        print(array[i])
        sift_down(array, i, len(array)-1)  # 初始化大顶堆

    print("初始化大顶堆结果:", array)
    # 交换堆顶与堆尾
    for head_end in range(len(array)-1, 0, -1):  # start stop step
        array[head_end], array[0] = swap(array[head_end], array[0]) # 交换堆顶与堆尾
        sift_down(array, 0, head_end-1)  # 堆长度减一(head_end-1),再从上往下调整成大顶堆



if __name__ == "__main__":
    array = [55,77,22,33,45]
    print(array)
    heap_sort(array)
    print("堆排序最终结果:", array)

堆排序是不稳定的,时间复杂度O(nlogn)

 

常见排序算法效率比较

排序方法平均情况最好情况最坏情况辅助空间稳定性
冒泡排序O(n2)O(n)O(n2)O(1)稳定
选择排序O(n2)O(n2)O(n2)O(1)不稳定
插入排序O(n2)O(n)O(n2)O(1)稳定
希尔排序O(nlogn) - O(n2)O(n1.3)O(n2)O(1)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定
快速排序O(nlogn)O(nlogn)O(n2)O(nlogn) - O(n)不稳定

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值