排序算法初探

总结

本文主要介绍常见排序及其python实现。试图用尽可能短的描述解释清楚几种排序算法,权当方便自己理解记忆。

排序方法平均时间复杂度最坏时间复杂度最好时间复杂度稳定性
选择排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2)不稳定
冒泡排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2)稳定
插入排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n)稳定
希尔排序 O ( n 1.3 ) O(n^{1.3}) O(n1.3) O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n)不稳定
归并排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)稳定
快速排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n 2 ) O(n^2) O(n2) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)不稳定
堆排序 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)不稳定

选择排序

将元素分为两部分,左边是已排序区,右边是待排序区,每次从待排序部分中选择最小的数与第一个待排序元素(也即已排序区后面的第一个元素)交换。
不管数组原先是否有序,时间复杂度都是 O ( n 2 ) O(n^2) O(n2)
不稳定举例:
(7) 2 5 9 3 4 [7] 1
利用选择排序算法进行排序时候,(7)和1调换,(7)就跑到了[7]的后面了,原来的次序改变了,因此不稳定。

def selection_sort(arr):
    l = len(arr)
    for i in range(l-1):  # 左边已排序边界
        min_index = i
        for j in range(i+1, l):  # 对于右边待排序元素,选出最小与最左交换
            if arr[j] < arr[min_index]:
                min_index = j
        arr[i], arr[min_index] = arr[min_index], arr[i]
    return arr

冒泡排序

将元素分为两部分,左边是待排序区,右边是已排序区。每次两两比较相邻元素,如果顺序不对,交换,这样一轮时候最大的数字被交换到最右。
不管数组原先是否有序,时间复杂度都是 O ( n 2 ) O(n^2) O(n2)

相同元素不交换能保证稳定。

def bubble_sort(arr):
    l = len(arr)
    for i in range(l-1, -1, -1):  # 已排序区边界
        for j in range(i):  # 对于待排序元素依次比较交换
            if arr[j] > arr[j + 1]:  # >保证稳定;如果是>=,则不稳定
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
    return arr

插入排序

左边是已排序区,右边是待排序区,对于每个未排序元素,将其与前面已排序元素依次对比(从右到左)之后插入到正确的位置。
当原数组有序时,是最佳情况,时间复杂度为 O ( n ) O(n) O(n)

将元素插入到排序区中时,如果遇到相等元素,结束交换,能保证稳定性。

def insert_sort(arr):
    l = len(arr)
    for i in range(1, l): # 对于右边待排序区
        num = arr[i]
        j = i - 1
        while j >= 0 and arr[j] > num:  # 如果待排序元素比排序区中元素小,应该查到排序区里面。 >保证稳定;如果是>=,则不稳定
            arr[j+1] = arr[j]  # 将已排序元素往后挪一位,给num腾位置
            j -= 1
        arr[j+1] = num
    return arr

希尔排序

先将整个序列分割成为若干子序列分别进行插入排序,待整个序列中的记录“基本有序”时,再对全体元素依次插入排序。

def shell_sort(arr):
    count = len(arr)
    step = 2
    group = count // step
    while group > 0:
        for i in range(group):
            j = i + group
            while j < count:
                k = j - group
                key = arr[j]
                while k >= 0:
                    if arr[k] > key:
                        arr[k + group] = arr[k]
                        arr[k] = key
                    k -= group
                j += group
        group //= step
    return arr

归并排序

将待排序数组分为两段,递归排序。使用辅助数组helper,需要额外空间,空间复杂度O(n)。

merge时,如果遇到两个元素相等,先拷贝左边的,能保证稳定性。

def merge(arr, L, mid, R):  # 对L~mid, mid+1~R合并
    i, j = L, mid + 1
    helper = []  # 辅助数组大小在所有过程加起来长度=len(arr),因此空间复杂度O(n),不过这里每次使用完都“销毁”
    while i <= mid and j <= R:
        if arr[i] <= arr[j]:  # 如果两个元素相等,先拷贝左边的,因此能保证稳定性
            helper.append(arr[i])
            i += 1
        else:
            helper.append(arr[j])
            j += 1
    helper += arr[i:mid+1]
    helper += arr[j:R+1]
    arr[L:R+1] = helper

def merge_sort(arr, L, R):
    # 归并排序,递归操作
    if L == R:
        return
    mid = (L + R) // 2
    merge_sort(arr, L, mid)  # 对左侧排序
    merge_sort(arr, mid+1, R)  # 对右侧排序
    merge(arr, L, mid, R)  # 左右两侧合并

def merge_sort_main(arr):
    L = 0
    R = len(arr) - 1
    merge_sort(arr, L, R)
    print(arr)

merge_sort_main([4,3,7,1,7,8,5])

快速排序

主要思想:

  1. partition操作:选取pivot,将要数组分成两部分,左边区域<=x,右边区域>x;
  2. 对这两部分数据分别重复步骤1。

主要操作:对数组arr从L到R位置进行partition操作——返回pivot最终的位置
我们指定最右边元素arr[R]为pivot,进行如下操作:

  1. 左边区域为<=区域,初始边界为L-1;
  2. 使用指针 i 初始指向起点L,比较arr[i]与arr[R]:
    如果arr[i] <= arr[R]:arr[i]与<=区域后面元素交换,<=区域扩大一位;
    每次比较完指针 i 右移,直到 i 到达R,结束;
  3. 最后,将arr[R]与<=区域后面元素交换,并返回arr[R]的位置。

图解partition:
partition
注意,使用快排空间复杂度是O(logn)。

def partition(arr, L, R): 
    less = L - 1  # 小于等于区域的右边界
    for i in range(L, R): # 遍历数组的指针i
        if arr[i] <= arr[R]:
            arr[i], arr[less+1] = arr[less+1], arr[i]
            less += 1
    arr[less+1], arr[R] = arr[R], arr[less+1]
    return less+1

def quick_sort(arr, L, R):
    if L < R:
        pivot = partition(arr, L, R)
        quick_sort(arr, L, pivot - 1)
        quick_sort(arr, pivot + 1, R)

def quick_sort_main(arr):
    l = len(arr)
    return quick_sort(arr, 0, l-1)
    
arr = [4, 1, 8, 10, 7, 3, 13, 9, 2]
quick_sort_main(arr)

最坏情况:数组已经排好序且每次pivot选取最右边元素。
解决:pivot随机选取,在quick_sort中添加两行index = random.randint(L, R), arr[index], arr[R] = arr[R], arr[index]即可。

三路快排:
问题:
如果重复的元素比较多,使用上面的快排方法会比较慢,因为对于等于pivot的元素需要重复比较。
解决:
partition操作改为将数组分为三个区域:小于区域,等于区域,大于区域。
partition3
改进后代码如下:

import random

def partition3(arr, L, R):
    less = L - 1
    more = R
    while L < more:
        if arr[L] < arr[R]:
            arr[L], arr[less + 1] = arr[less + 1], arr[L]
            less += 1
            L += 1
        elif arr[L] > arr[R]:
            arr[L], arr[more - 1] = arr[more - 1], arr[L]
            more -= 1
        else:
            L += 1
    arr[R], arr[more] = arr[more], arr[R]
    return [less + 1, more]

def quick_sort(arr, L, R):
    if L < R:
        index = random.randint(L, R)
        arr[index], arr[R] = arr[R], arr[index]
        pivot1, pivot2 = partition3(arr, L, R)
        quick_sort(arr, L, pivot1 - 1)
        quick_sort(arr, pivot2 + 1, R)

def quick_sort_main(arr):
    return quick_sort(arr, 0, len(arr) - 1)

arr = [4, 1, 8, 10, 7, 3, 13, 9, 2]
quick_sort_main(arr)

堆排序

建立最大堆后,每次把根节点换到未排序的节点,因此越往后是越大的,用来升序排序;
同理,建立最小堆,用来降序排序。

对于用数组表示的二叉树中的节点i,且父节点下标为(i-1)//2,左孩子为2i+1,右孩子为2i+2。

通过建立最大堆得到升序排序:

  1. 建最大堆:从倒数第二行末尾(下标为n//2)开始,判断其是否大于左右孩子,如不满足,需要交换,最后得到最大堆;
  2. 排序:认为堆的末尾为已排序区域,将根节点(未排序区域最大值)与排序区前一个元素交换,排序区扩大一位,交换后需调整堆使其继续满足最大堆,重复该操作直到排序区域扩展到根节点,说明已经完成排序。

注意,在建立最大堆的过程,就可能将相等元素原先的相对位置破坏,因此不稳定。

def adjust_heap(arr, i, up):
    # i:需要调整的节点
    # up:只调整arr[0, up-1],如果调整整个列表是arr[0,n-1]
    while 2 * i + 1 < up:  # 如果有左子节点
        son = 2 * i + 1
        if son + 1 < up and arr[son + 1] > arr[son]:  # 先把son指向最大的儿子
            son += 1
        if arr[i] < arr[son]:  # 判断是否需要交换
            arr[i], arr[son] = arr[son], arr[i]
            i = son
        else:
            break

# 建最大堆操作
def build_heap(arr):
    n = len(arr)
    for i in range(n // 2)[::-1]:
        adjust_heap(arr, i, n)

def heap_sort(arr):
    n = len(arr)
    build_heap(arr)
    for i in range(n)[::-1]:
        arr[i], arr[0] = arr[0], arr[i]  # 交换根节点和待排序的最后一个节点
        adjust_heap(arr, 0, i)  # 交换完后调整树,注意已排序的不需要调整了
    print(arr[::-1])  # 从大到小排序
    print(arr)  # 从小到大排序

heap_sort([23, 1, 5, 3, 2, 6, 26])

拓展

下面是排序算法的一些拓展应用题目:

  1. 数组中的第K个最大元素
  2. 最小的K个数
  3. 数据流中的中位数
  4. 小和问题,数组的逆序对
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值