复习一下几个基本的排序算法

1.冒泡排序

from typing import *

T = TypeVar('T')

def bubble_sort(arr: list[T], cmp_func: Callable[[T, T], bool]):
    """
    + 假设数组`arr`长度为n,冒泡排序算法流程如下:
    1) 对n个元素执行“冒泡”,将最大(最小)元素交换至正确位置
    2) 对n-1个元素执行“冒泡”,将最大(最小)元素交换至正确位置
    3) 重复以上过程,经过n-1轮“冒泡”后,数组前n-1大(小)元素已正确排序
    4) 仅剩的一个元素必然是最小(最大),无需排序,因此整个数组排序完成

    + 算法特性:
    1) 时间复杂度:O(n^2)
    2) 空间复杂度:O(1),原地排序,无需额外空间
    3) 稳定性排序,交换过程不会改变相同元素在数组中的相对位置
    """
    if len(arr) <= 1:
        return 
    n = len(arr)
    # 外层循环,未排序区间[0, i]
    for i in range(n-1, 0, -1):
        flag = False # 标记本轮冒泡过程是否发生交换
        # 内层循环;将未排序区间的最大(最小)元素交换到区间最右侧
        for j in range(i):
            if cmp_func(arr[j+1], arr[j]):
                arr[j+1], arr[j] = arr[j], arr[j+1]
                flag = True
        if not flag: # 本轮未发生交换,则直接跳出
            break

2.插入排序

from typing import *

T = TypeVar('T')

def insertion_sort(arr: list[T], cmp_func: Callable[[T, T], bool]):
    """
    + 假设数组`arr`大小为n,选择排序算法流程如下:
    1) 初始状态下,数组的第一个元素已经有序
    2) 选取数组的第2个元素作为base,将其插入到合适位置,此时数组前两个元素有序
    3) 选取数组的第3个元素作为base,将其插入到合适位置,此时数组前三个元素有序
    4) 重复以上过程,在最后一轮中,选取最后一个元素作为base,将其插入到合适位置后,数组整体有序
    
    + 算法特性:
    1) 时间复杂度:O(n^2)
    2) 空间复杂度:O(1),无需额外空间
    3) 稳定性排序,交换过程不会改变相同元素在数组中的相对位置
    4) 尽管插入排序的时间复杂度较高,但在数据量较小的情况下,插入排序通常更快。
    """
    if len(arr) <= 1:
        return 
    n = len(arr)
    # 外层循环,已排序区间[0, i-1]
    for i in range(1, n):
        base = arr[i]
        j = i - 1
        # 内层循环,将base插入到已排序区间[0, i-1]中的合适位置
        while j >= 0 and cmp_func(base, arr[j]):
            arr[j+1] = arr[j] # 将第j个元素向后移一位
            j -= 1
        arr[j+1] = base # 将base赋值到正确位置

3.选择排序

from typing import *

T = TypeVar('T')

def selection_sort(arr: list[T], cmp_func: Callable[[T, T], bool]):
    """
    + 设数组`arr`长度为n,选择排序算法流程如下:
    1) 初始状态下,所有元素均为排序,即未排序区间为[0, n-1]
    2) 选取区间[0, n-1]中最小(最大)的元素,将其与第0个元素交换,完成后数组前1个数据有序
    3) 选取区间[1, n-1]中最小(最大)的元素,将其与第1个元素交换,完成后数组前2个数据有序
    4) 重复以上过程,经过n-1轮选择和交换后,数组的前n-1个元素有序
    5) 仅剩的最后一个元素必然是整个数组的最小(最大)元素,无需排序,因此数组排序完成
    
    + 算法特性:
    1) 时间复杂度: O(n^2)
    2) 空间复杂度: O(1),原地排序,无需额外空间
    3) 非稳定性排序,交换过程中相同元素在数组中的相对位置可能会发生改变
    """
    n = len(arr)
    # 外层循环n-1次,未排序区间[i, n-1]
    for i in range(n - 1):
        k = i 
        # 内层循环找到未排序区间最小(最大)值索引
        for j in range(i+1, n):
            if cmp_func(arr[j], arr[k]):
                k = j # 选择未排序区间最小(最大)值索引
        # 与区间首个元素交换
        arr[i], arr[k] = arr[k], arr[i]

4. 归并排序

def merge(arr: list[int], left: int, mid: int, right: int):
    # 合并左子数组[left, mid]和右子数组[mid+1, right]
    tmp_arr = [0] * (right - left + 1)  # 临时数组存储排序结果
    # 每次取两个子数组中较小的数放入临时数组中
    i, j, k = left, mid + 1, 0
    while i <= mid and j <= right:
        if arr[i] < arr[j]:
            tmp_arr[k] = arr[i]
            i += 1
        else:
            tmp_arr[k] = arr[j]
            j += 1
        k += 1
    # 将左子数组和右子数组剩余的元素放入临时数组中
    while i <= mid:
        tmp_arr[k] = arr[i]
        i += 1
        k += 1
    while j <= right:
        tmp_arr[k] = arr[j]
        j += 1
        k += 1
    # 将已经排好序的临时数组元素复制回原数组对应的区间
    for i in range(len(tmp_arr)):
        arr[left + i] = tmp_arr[i]

def merge_sort(arr: list[int], left: int, right: int):
    """
    + 归并排序算法分为“递归”和“合并”两个阶段:
    1) 递归阶段:通过递归不断地将数组从中点处分开,将长数组的排序问题转换为短数组的排序问题
    2) 合并阶段:当子数组长度为 1 时终止划分,开始合并,持续地将左右两个较短的有序数组合并为
    一个较长的有序数组,直至结束。

    + 算法特性:
    1) 时间复杂度:O(NlogN)
    2) 空间复杂度:O(N),非原地排序,合并过程需要借助辅助数组实现,需要使用O(N)大小的额外空间
    3) 稳定排序:合并过程中相同元素的次序不变
    """
    if left >= right:
        return 
    
    mid = left + (right - left)//2
    merge_sort(arr, left, mid)
    merge_sort(arr, mid+1, right)
    merge(arr, left, mid, right)

5. 快速排序

from typing import *


def median_three(arr: list[int], left: int, mid: int, right: int) -> int:
    """三数取中优化"""
    l, m, r = arr[left], arr[mid], arr[right]
    if (l <= m <= r) or (r <= m <= l):
        return mid
    if (r <= l <= m) or (m <= l <= r):
        return left
    return right

def partition(arr: list[int], left: int, right: int) -> int:
    """
    + 哨兵划分算法流程如下:
    1) 选取数组最左端元素arr[left]为基准(或采取三数取中),初始化两个指针i和j分别指向数组两端
    2) 设置一个循环,每轮循环中使用i(j)寻找第一个比基准大(小)的位置,然后交换这两个位置上的元素
    3) 循环执行步骤2),直到两个指针相遇时停止,将基准元素与两个子数组分界位置元素交换,此时基准元素已处于正确位置
    4) 返回基准元素的索引
    """
    median = median_three(arr, left, left+(right-left)//2, right)
    # 将中位数与left交换
    arr[left], arr[median] = arr[median], arr[left]
    pivot = arr[left]
    i, j = left, right
    while i < j:
        while i < j and arr[j] >= pivot:
            j -= 1 # 从右向左找第一个小于基准的元素
        while i < j and arr[i] <= pivot:
            i += 1 # 从左到右找第一个大于基准的元素
        arr[i], arr[j] = arr[j], arr[i] # 交换元素
    # 将基准元素交换至两子数组分界位置
    arr[i], arr[left] = arr[left], arr[i]
    # 返回基准元素位置索引
    return i

def quick_sort(arr: list[int], left: int, right: int):
    """
    + 快速排序算法流程如下:
    1) 首先对原数组执行一次"哨兵划分",得到未排序的左子数组和右子数组
    2) 然后,对左子数组和右子数组分别递归执行"哨兵划分"过程
    3) 持续递归,直到子数组长度为1,此时整个数组排序完成

    + 算法特性:
    1) 时间复杂度:O(NlogN),最坏情况时间复杂度O(N^2)  
    2) 空间复杂度:O(N),原地排序, 尾递归优化:O(logN)  
    3) 非稳定性排序,在哨兵划分的最后一步,基准数可能会被交换至相等元素的右侧。
    """
    # 尾递归优化
    # 子数组长度为1时终止
    while left < right:
        # 哨兵划分操作
        pivot = partition(arr, left, right)
        # 对两个子数组中较短的那个执行队递归
        if pivot - left < right - pivot:
            quick_sort(arr, left, pivot-1) # 递归排序左数组
            left = pivot + 1              # 未排序区间[pivot+1, right]
        else:
            quick_sort(arr, pivot+1, right) # 递归排序右数组
            right = pivot - 1              # 未排序区间[left, pivot-1]

6.堆排序

def heap_sort(arr: list[int]):
    """
    + 堆排序算法流程:
    1) 输入数组并建立大顶堆。完成后,最大元素位于堆顶
    2) 将堆顶元素(第一个元素)与堆底元素(最后一个元素)交换。完成交换后,堆的长度减1,
    已排序元素数量加1
    3) 从堆顶元素开始,从顶到底执行堆化操作(sift down)。完成堆化后,堆的性质得到修复。
    4) 循环执行第(2)步和第(3)步。循环`n-1`轮后,即可完成数组排序。

    + 算法特性:
    1) 时间复杂度:O(NlogN)
    2) 空间复杂度:O(1),原地排序,交换元素和堆化操作都是在原数组上进行的
    3) 非稳定性排序:交换堆顶和堆底元素时,相同元素的相对位置可能会发生改变
    """
    # 建大顶堆:堆化除叶节点以外的所有子节点
    for i in range(len(arr)//2, -1, -1):
        _adjust_down(arr, i, len(arr))
    # 从堆中提取最大元素(堆顶元素),循环n-1轮
    for i in range(len(arr)-1, 0, -1):
        # 交换堆顶与堆底
        arr[0], arr[i] = arr[i], arr[0]
        # 从根节点开始向下调整
        _adjust_down(arr, 0, i)


def _adjust_down(arr: list[int], i: int, n: int):
    # 大顶堆调整算法
    # n: 数组arr有效长度; i: 待调整的节点索引
    while True:
        # 从节点i及其左右子节点中找出最大值节点
        pa = i
        left = 2 * i + 1
        right = 2 * i + 2
        if left < n and arr[left] > arr[pa]: # 
            pa = left
        if right < n and arr[right] > arr[pa]:
            pa = right

        if pa == i: # 不需要调整
            break
        # 将最大值交换到父节点i
        arr[i], arr[pa] = arr[pa], arr[i]
        # 循环向下调整
        i = pa

7. 桶排序

def bucket_sort(arr: list[int], bucket_nums: int = 5):
    """
    + 桶排序算法流程如下:
    1) 初始化k个桶,将n个元素分配到k个桶中。
    2) 对每个桶分别执行排序
    3) 按照桶从小到大的顺序合并结果。

    + 算法特性:
    1) 桶排序适用于处理体量很大的数据。
    2) 时间复杂度:O(N + K)
    3) 自适应排序:在最差情况下,所有数据被分配到一个桶中,且排序该桶使用O(N^2)时间
    4) 空间复杂度:O(N + K),非原地排序
    5) 桶排序是否稳定取决于排序桶内元素的算法是否稳定。
    """
    if len(arr) <= 1:
        return
    
    buckets = [[] for _ in range(bucket_nums)] # 初始化bucket_nums个桶
    min_val, max_val = float("inf"), -float("inf")
    for num in arr:
        if num < min_val:
            min_val = num
        if num > max_val:
            max_val = num
        
    # 将数组元素分配到各个桶内
    for num in arr:
        i = int((num-min_val)*(bucket_nums-1)/max_val)
        buckets[i].append(num)

    # 对各个桶分别执行排序
    i = 0
    for bucket in buckets:
        if len(bucket) > 0:
            insertion_sort(bucket, lambda x1, x2 : x1 < x2) # 调用插入排序算法对桶内元素排序
            for num in bucket:
                arr[i] = num
                i += 1

8. 计数排序

def counting_sort(arr: list[int]):
    """
    + 计数排序算法完整流程如下:
    1) 遍历数组,找出其中的最大数字,记为`m`,然后创建一个长度为`m+1`的辅助数组`counter`
    2) 借助`counter`统计`arr`中各数字的出现次数,其中`counter[num]`对应数字`num`的出现次数
    3) 计算`counter`的前缀和数组`prefix`,前缀和数组的含义:`prefix[num]-1`表示元素`num`在
    结果数组`res`中最后一次出现的索引
    4) 倒序遍历原数组`arr`中的每个元素`num`,将`num`填入数组`res`的索引`prefix[num]-1`处,令
    前缀和`prefix[num]`减小1,得到下次放置`num`的索引
    5) 遍历完成后,数组`res`中存放的就是排序好的结果,最后将其覆盖原数组`arr`

    + 算法特性:
    1) 时间复杂度:O(N + M)
    2) 空间复杂度:O(N + M),非原地排序
    3) 稳定性排序:由于向`res`中填充元素的顺序是“从右向左”的,因此倒序遍历`arr`可以避免改变相等
    元素之间的相对位置
    """
    if len(arr) <= 1:
        return 
    
    # 1. 统计原数组最大值
    max_val = -float("inf")
    for num in arr:
        max_val = max(max_val, num)

    # 2. 统计原数组每个元素出现的次数
    counter = [0 for _ in range(max_val + 1)]
    for num in arr:
        counter[num] +=1

    # 3. 计算counter的前缀和
    for i in range(len(counter)-1):
        counter[i+1] += counter[i]

    # 4.倒序遍历arr
    res = [0 for _ in range(len(arr))]
    for i in range(len(arr)-1, -1, -1):
        num = arr[i]
        res[counter[num] - 1] = num
        counter[num] -= 1

    # 5. 用res覆盖arr
    for i in range(len(arr)):
        arr[i] = res[i]

9. 基数排序

def _digit(num: int, exp: int):
    # 获取num第k位上的数字,其中,exp=10^(k-1)
    return (num // exp) % 10

def _counting_sort_digit(arr: list[int], exp: int):
    # 对arr中的元素按照第k位上的数字进行计数排序
    counter = [0] * 10
    for num in arr:
        d = _digit(num, exp)
        counter[d] += 1

    for i in range(1, 10):
        counter[i] += counter[i-1]
    res = [0] * len(arr)
    for i in range(len(arr)-1, -1, -1):
        num = arr[i]
        d = _digit(num, exp)
        idx = counter[d] - 1
        res[idx] = num
        counter[d] -= 1

    for i in range(len(arr)):
        arr[i] = res[i]


def radix_sort(arr: list[int]):
    """
    + 假设数字的最低位是第1位,基数排序算法流程如下:
    1) 初始化k=1
    2) 对数字的第k位进行“计数排序”,完成后,数据会按照第k位从小到大排序
    3) 将k增加1,返回步骤2),继续迭代,直到所有位都排序后结束

    + 算法特性:
    1) 相较于计数排序,基数排序适用于数值范围较大的情况,但前提是数据必须可以
    表示为固定位数的格式,且位数不能过大
    2) 时间复杂度:O(nk), n为数据量,k为数据最大位数
    3) 空间复杂度:O(n + d),d为进制数
    4)排序稳定性取决于内部计数排序算法的稳定性
    """
    m = max(arr)
    # 按照低位到高位的顺序遍历
    exp = 1
    while exp <= m:
        # 对数组元素的第k位进行计数排序
        _counting_sort_digit(arr, exp)
        exp *= 10 # 循环遍历下一位

参考:Hello 算法

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值