八大排序算法特点与实现 [Python]

164 阅读5分钟

引入

排序算法是算法学习中最基础的一部分,更是面试的常见考题。

掌握各种常用排序算法的特点,能够熟练地手撕,才能在面试中表现的更好。

本文列举:冒泡排序、选择排序、插入排序、归并排序、快速排序、希尔排序、堆排序、基数排序,共八大常用排序算法的方法特点,均使用Python实现。

1. 冒泡排序

1.1 基本特点

冒泡排序进行n轮(n为序列长度-1)冒泡

每轮冒泡将最小(大)的元素交换到待排序序列的最后(最前面)

若当前轮次没有进行任何交换则序列已经有序

注意:

  1. 最小还是最大要看比较条件是小于还是大于
  2. 最后还是最前要看比较是从前往后还是从后往前

1.2 算法分析

  • 稳定性:稳定
  • 时间复杂度 :最佳:O(n)O(n) ,最差:O(n2)O(n^2), 平均:O(n2)O(n^2)
  • 空间复杂度 :O(1)O(1)

1.3 代码实现

def bubble_sort(arr):
    for i in range(len(arr)):
        flag = False  # 指示本轮是否改变了顺序
        for j in range(len(arr)-i-1):
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
                flag = True
        if not flag: # 未改变顺序,已有序
            return

2. 选择排序

2.1 基本特点

选择排序是一种简单直观的排序算法。

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。

然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

以此类推,直到所有元素均排序完毕。

2.2 算法分析

  • 稳定性:不稳定
  • 时间复杂度 :最佳:O(n2)O(n^2) ,最差:O(n2)O(n^2), 平均:O(n2)O(n^2)
  • 空间复杂度 :O(1)O(1)

2.3 代码实现

def selection_sort(arr):
    for i in range(len(arr)):
        m = i
        for j in range(i+1, len(arr)):  # 找出未排序序列中最小的元素
            if arr[j]< arr[m]:
                m = j
        if m!=i:
            arr[i], arr[m] = arr[m], arr[i] # 将其交换到对应的位置

3. 插入排序

3.1 基本特点

插入排序的排序方式有点类似于扑克牌整理手牌的方式

(但有一些不同,插入排序在寻找插入位置的同时就在一步步挪动元素来腾空间)

对于每个未排序的元素,先在已排序序列中从后向前扫描并挪动元素腾出位置,直到该元素移动到正确的位置(原理上是找到位置再插入,但操作上更高效)

3.2 算法分析

  • 稳定性:稳定
  • 时间复杂度 :最佳:O(n) ,最差:O(n2), 平均:O(n2)
  • 空间复杂度 :O(1)

3.3 代码实现

def insertion_sort(arr):
    for i in range(1, len(arr)):  # 将第一个元素视为有序序列
        cur = arr[i]
        j = i-1
        while j>=0 and cur <arr[j]:
            arr[j+1] = arr[j] # 后移
            j-=1
        arr[j+1] = cur

3.4 优化:折半插入排序

与原先的直接插入排序差别在于:

先使用折半查找出元素的待插入位置,然后再统一挪动插入位置之后的所有元素

def binary_insertion_sort(arr):
    for i in range(1, len(arr)):  # 将第一个元素视为有序序列
        l, r = 0, i-1  # 位置i对应当前要插入的元素
        while (l <= r):
            mid = (l+r)//2
            if arr[i] < arr[mid]:
                r = mid-1  # 在左子表中
            else:
                l = mid+1  # 在右子表中

        # 遇到这种不好定范围的,建议在纸上画特例
        j = i-1
        cur = arr[i]
        while j > r:
            arr[j+1] = arr[j] # 后移
            j -= 1
        arr[j+1] = cur # 插入

4. 希尔排序

4.1 基本特点

因为直接插入排序适用于基本有序、数据的排序表,基于此希尔排序是对直接插入排序的一种优化,又称作缩小增量排序。

希尔排序的基本思想是将相隔某个“增量”的记录组成一个子表,对子表分别进行直接插入排序

当整个表中的元素已呈“基本有序”时,再对全体记录进行一次直接插入排序

常见的增量取dk=len(arr)//2,每轮排序后将增量缩小为一半。

直到增量为1时,相当于对全体记录进行直接插入排序,从而完成排序。

4.2 算法分析

  • 稳定性:稳定
  • 时间复杂度 :最佳:O(nlogn)O(nlogn), 最差:O(n2)O(n^2) 平均:O(nlogn)O(nlogn)
  • 空间复杂度 :O(1)O(1)

4.3 代码实现

def shell_sort(arr):
    dk = len(arr)//2
    while (dk >= 1):
        for i in range(dk, len(arr)): # 分轮次
            cur = arr[i] # 对每轮的当前元素进行直接插入排序
            j = i - dk # 注意间隔为dk
            while j>=0 and cur < arr[j]:
                arr[j+dk] = arr[j]
                j -= dk
            arr[j+dk] = cur
        dk //= 2

5. 归并排序

5.1 基本特点

归并排序,“归并”的含义是将两个或两个以上的有序表组合成一个新的有序表

该算法是分治法 的一个非常典型的应用,利用递归来实现

先进行分解并排序,然后进行合并(合并时还需要调整位置以保证仍然有序)

5.2 算法分析

  • 稳定性:稳定
  • 时间复杂度 :最佳:O(nlogn)O(nlogn), 最差:O(nlogn)O(nlogn), 平均:O(nlogn)O(nlogn)
  • 空间复杂度 :O(n)O(n)

5.3 代码实现

def merge_sort(arr):
    def merge(arr1, arr2):
        res = []
        i, j = 0, 0
        l1, l2 = len(arr1), len(arr2)
        while i < l1 and j < l2:
            if arr1[i] <= arr2[j]:
                res.append(arr1[i])
                i += 1
            else:
                res.append(arr2[j])
                j += 1

        # 还有有一个数组未完全合并
        if i < l1:
            res += arr1[i:]
        if j < l2:
            res += arr2[j:]

        return res

    if len(arr) <= 1:return arr
    mid = len(arr)//2
    
    # 拆分成左右两部分,递归进行分解、排序、合并
    return merge(merge_sort(arr[:mid]), merge_sort(arr[mid:]))

6. 快速排序

6.1 基本特点

快速排序的基本思想也是基于分治法的

每次在待排序表中任取一个元素作为基准(通常取首元素)

然后通过一轮排序将排序表划分为独立的两个部分,得到基准的最终位置:

  1. 小于基准的部分
  2. 大于等于基准的部分(基准位于该部分)

对两部分递归地重复多轮操作,直到两部分都只有一个元素或空(终止条件)

特点是每轮能将基准元素放到其最终的位置上

6.2 算法分析

  • 稳定性 :不稳定
  • 时间复杂度 :最佳:O(nlogn)O(nlogn), 最差:O(nlogn)O(nlogn),平均:O(nlogn)O(nlogn)
  • 空间复杂度 :O(nlogn)O(nlogn)

6.3 代码实现

def quick_sort(arr):
    def partition(l, r):
        p = arr[l] # 使用左端为基准(所以下方从右端开始搜索)
        while l < r:
            # 使用双指针交替移动到左右端

            # 从右往左搜索小于基准的(注意左右端不能都搜索相等的)
            while l < r and arr[r] >= p:
                r -= 1
            arr[l] = arr[r]  # 将其移动到左端

            # 从左往右搜索大于等于基准的(注意左右端不能都搜索相等的)
            while l < r and arr[l] < p:
                l += 1
            arr[r] = arr[l]  # 移动到右端

        # 直到l==r时停止,此时的位置即基准的最终位置
        arr[l] = p
        return l

    def quick_sort_between(l, r):
        if l >= r:  # 元素数量只有1个或者0个时递归结束
            return

        m = partition(l, r)  # arr[m] 作为划分标准
        quick_sort_between(l, m - 1)
        quick_sort_between(m + 1, r)

    quick_sort_between(0, len(arr)-1)

7. 堆排序

7.1 基本特点

堆排序是指利用堆来设计的一种排序算法。

堆是一个近似完全二叉树的结构,子结点的值总是小于(或者大于)它的父节点,分为小根堆和大根堆。

堆排序的核心步骤:

  1. 将数组调整为大顶堆,该过程即建堆
  2. 将堆顶元素与最后一个元素进行交换(即输出堆顶元素到序列尾),然后对其他元素再次建堆
  3. 重复上面的处理流程,直到堆中仅剩下一个元素,此时序列已有序。

此排序方式可以解决找出最大(小)的n个数的问题,适合数据量大时使用。

7.2 算法分析

  • 稳定性 :不稳定
  • 时间复杂度 :最佳:O(nlogn)O(nlogn), 最差:O(nlogn)O(nlogn), 平均:O(nlogn)O(nlogn)
  • 空间复杂度 :O(1)O(1)

7.3 代码实现

def heap_sort(arr):
    def build_min_heap(root, end):
        while True:
            child = 2 * root + 1
            if child > end:  # 无左子节点
                return
            # 若右子节点存在,则找出左右中更小的
            if (child+1 <= end) and (arr[child+1] < arr[child]):
                child += 1

            if arr[child] < arr[root]:
                arr[child], arr[root] = arr[root], arr[child] # 交换两者顺序
                root = child # 继续向下调整
            else:
                return

    n = len(arr)
    first_root = n // 2 - 1  # 找到最后一个非叶节点(注意用//)
    for r in range(first_root, -1, -1):  # 由后向前遍历所有的根节点,建堆调整
        build_min_heap(r, n - 1)

    # 调整完成后,输出到序列尾,然后再次调整堆
    for end in range(n - 1, 0, -1):
        arr[0], arr[end] = arr[end], arr[0]
        build_min_heap(0, end - 1)

8. 基数排序

8.1 基本特点

基数排序是非比较的排序算法

将所有待比较数值统一为同样的数位长度(补零

按照低位先排序,分别放入10个队列中,然后采用先进先出的原则进行收集;再按照高位排序,然后再收集;依次类推,直到最高位,最终得到排好序的数列

对于整数可以使用上述方式,其他数据类型需要进行调整(找出特征值、优先级)

8.2 算法分析

  • 稳定性 :稳定
  • 时间复杂度 :最佳:O(n×k)O(n×k) 最差:O(n×k)O(n×k) 平均:O(n×k)O(n×k)
  • 空间复杂度 :O(n+k)O(n+k)

8.3 代码实现

def radix_sort(arr):
    # 找出最大数的位数 
    max_len = len(str(max(arr)))

    for i in range(max_len): # i代表当前位
        buckets = [[] for _ in range(10)] # 初始化0-9十个桶
        for num in arr:
            radix = int(num/(10**i) % 10) # int目的是将位数不够的数结果转为0
            buckets[radix].append(num) # 放入对应的桶
        j = 0
        for k in range(10): # 按顺序对桶进行收集
            for num in buckets[k]:
                arr[j] = num
                j += 1
    return arr