数据结构与算法:常见排序算法及其python实现

文章目录

  • 0、 综合分析
  • 1、冒泡排序(Bubble Sort)
  • 2、直接插入排序(Insertion Sort)
  • 3、选择排序(Selection Sort)
  • 4、希尔排序(Shell Sort)——插入排序升级
  • 5、快速排序(Quick Sort)——冒泡排序升级
  • 6、归并排序(Merge Sort)
  • 7、堆排序(Heap Sort)——选择排序升级
  • 8、计数排序(Counting Sort)
  • 9、基数排序(Radix Sort)
  • 10、桶/箱排序(Bucket Sort)

参考博客:https://www.cnblogs.com/onepixel/articles/7674659.html


0、 综合分析

0.1 排序算法的种类及时间限制
常见排序算法一般分为非线性时间比较类排序线性时间非比较类排序
比较类排序算法时间复杂度的下限为 O ( n log ⁡ n ) O(n\log n) O(nlogn),非比较类排序算法不受比较式排序算法的时间下限约束,可达到线性时间 O ( n ) O(n) O(n)

数据结构与算法:常见排序算法及其python实现_第1张图片

0.2 排序算法的复杂度

排序方法 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 稳定性 备注
冒泡排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) 稳定
插入排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1) 稳定
选择排序 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 ( 1 ) O(1) O(1) 不稳定
希尔排序 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 ( 1 ) O(1) O(1) 不稳定
快速排序 O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( log ⁡ n ) ∼ O ( n ) O(\log n)\sim O(n) O(logn)O(n) 不稳定
归并排序 O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n ) O(n) O(n) 稳定
堆排序 O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( 1 ) O(1) O(1) 不稳定
计数排序 O ( n + m ) O(n+m) O(n+m) O ( n + m ) O(n+m) O(n+m) O ( n + m ) O(n+m) O(n+m) O ( n + m ) O(n+m) O(n+m) 稳定 m m m为数值区间
桶排序 O ( n + n log ⁡ ( n / m ) ) O(n+n\log(n/m)) O(n+nlog(n/m)) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n ) O(n) O(n) O ( n + m ) O(n+m) O(n+m) 不确定 m m m为桶数
基数排序 O ( d ( n + m ) ) O(d(n+m)) O(d(n+m)) O ( d ( n + m ) ) O(d(n+m)) O(d(n+m)) O ( d ( n + m ) ) O(d(n+m)) O(d(n+m)) O ( n + m ) O(n+m) O(n+m) 稳定 m m m为基数, d d d为位数

1、冒泡排序(Bubble Sort)

1.1 基本思想
从待排序序列起始位置开始,从前往后依次比较各位置和其后一位置的大小,若当前位置的值大于其后一位置的值,则将它们的值交换。每趟冒泡排序后,最大值沉到最底部,较小值上浮。继续对去除最后一个元素的序列执行冒泡排序,直至仅剩一个元素。

优化版本:若一趟冒泡排序中没有元素交换(序列已有序),则程序提前终止。

1.2 算法分析
冒泡排序为稳定的排序算法,为比较类中的交换排序。
时间复杂度: 优化版的冒泡排序,若原序列已有序,时间复杂度为 O ( n ) O(n) O(n);最坏的情况是原序列逆序,时间复杂度 O ( n 2 ) O(n^2) O(n2)
空间复杂度: 仅需要一个空间用于两两交换的缓存,空间复杂度为 O ( 1 ) O(1) O(1)

1.3 动态演示

1.4 python实现

def bubble_sort(arr):
    """冒泡排序"""
    count = len(arr)
    for i in range(count - 1):
        for j in range(count - 1 - i):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]


def opt_bubble_sort(arr):
    """冒泡排序——优化版, 有序时提前结束循环"""
    count = len(arr)
    for i in range(count - 1):
        unchanged = True
        for j in range(count - 1 - i):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                unchanged = False

        if unchanged:
            break


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    bubble_sort(array)
    print(array)

2、直接插入排序(Insertion Sort)

2.1 基本思想
每次将一个新数据插入到前面有序序列的适当位置并使序列保持有序,直到待排序数据全部插入。初始有序序列是长度为1的序列。

2.2 算法分析
直接插入为稳定的排序算法。
时间复杂度: 若原序列已有序,每个元素仅需比较一次,时间复杂度为 O ( n ) O(n) O(n);最坏的情况是原序列逆序,时间复杂度 O ( n 2 ) O(n^2) O(n2)
空间复杂度: 仅需要一个空间存储待插入的数据,空间复杂度为 O ( 1 ) O(1) O(1)

2.3 动态演示

2.4 python实现

def insertion_sort(arr):
    """插入排序"""
    count = len(arr)
    for i in range(1, count):
        j, value = i - 1, arr[i]
        while j >= 0 and arr[j] > value:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = value


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    insertion_sort(array)
    print(array)

3、选择排序(Selection Sort)

3.1 基本思想
每次找到序列中最小元素位置,交换序列起始元素和最小元素。然后对去除首元素的序列重复执行上述过程,直至序列仅剩一个元素。

3.2 算法分析
直接选择排序为不稳定排序,如对{90,91,2}进行选择排序,第一趟排序完成后序列变为{2,91,90}。
时间复杂度: 任何情况的时间复杂度均为 O ( n 2 ) O(n^2) O(n2)
空间复杂度: 仅需要一个空间用于两两交换的缓存,空间复杂度为 O ( 1 ) O(1) O(1)

3.3 动态演示

3.4 python实现

def selection_sort(arr):
    """选择排序"""
    count = len(arr)
    for i in range(count - 1):
        min_index = i
        for j in range(i + 1, count):
            if arr[j] < arr[min_index]:
                min_index = j
        arr[min_index], arr[i] = arr[i], arr[min_index]


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    selection_sort(array)
    print(array)

4、希尔排序(Shell Sort)——插入排序升级

4.1 基本思想
简单插入排序的改进版,优先比较距离较远的元素,希尔排序又称缩小增量排序

将序列中的元素按下标的一定增量分组(初始分组数为n/2),每组各自使用直接插入排序。随后,不断减少分组的数量(新分组数为上一分组数的1/2),重复执行上述过程,直至仅剩一个分组。

4.2 动态演示(相同颜色为同一分组)

4.3 python实现

def shell_sort(arr):
    """希尔排序"""
    count = len(arr)
    gap = int(count / 2)
    while gap > 0:
        for i in range(gap, count):
            j, value = i - gap, arr[i]
            while j >= 0 and arr[j] > value:
                arr[j + gap] = arr[j]
                j -= gap
            arr[j + gap] = value
        gap >>= 1


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    shell_sort(array)
    print(array)

5、快速排序(Quick Sort)——冒泡排序升级

5.1 基本思想
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后按此方法对这两部分数据再次分割,整个排序过程可以递归进行,以此达到整个序列有序。

5.2 算法实现
(1) 程序开始时,若数组长度小于2则返回,否则设置i和j分别为数组首尾位置,且将数组首元素作为 哨兵元素 value;
(2) 若i小于j,则 从位置j开始向前查找,不断递减j,直到找到小于value的元素,将j位置元素赋给i位置,i加1;
(3) 若i小于j,则 从位置i开始向后查找,不断递增i,直到找到大于value的元素,将i位置元素赋给j位置,j减1;
(4) 若i小于j,则重复步骤2和3,否则将哨兵数据赋给位置i,并递归调用上述过程处理i前后两段数组,程序返回;

5.3 算法分析
快排可看作为冒泡排序的改进版,其 对有序或逆序序列排序时(每次选取序列的最大或最小值),退化为冒泡排序。

最优时间复杂度
递归算法的时间复杂度公式: T [ n ] = a T [ n / b ] + f ( n ) \mathcal T[n]=a\mathcal T[n/b]+f(n) T[n]=aT[n/b]+f(n)

快排最优的情况下是每次选取数组的中位数作为哨兵元素,此时一次迭代完成后数组被平分为2个子数组。
此时时间复杂度公式为: T [ n ] = 2 T [ n / 2 ] + f ( n ) \mathcal T[n] = 2\mathcal T[n/2]+f(n) T[n]=2T[n/2]+f(n),其中 T [ n / 2 ] \mathcal T[n/2] T[n/2]为排序子数组所需时间, f ( n ) f(n) f(n)为平分当前数组所需时间。
迭代推导最优时间复杂度:
T [ n ] = 2 T [ n / 2 ] + n = 2 ( 2 T [ n / 4 ] + n / 2 ) + n = ⋯ = 2 m T [ n / 2 m ] + m n \mathcal T[n]=2\mathcal T[n/2]+n = 2(2\mathcal T[n/4]+n/2)+n = \cdots=2^m\mathcal T[n/2^m]+mn T[n]=2T[n/2]+n=2(2T[n/4]+n/2)+n==2mT[n/2m]+mn

显然,当数组仅剩下一个元素,即当 n = 2 m n=2^m n=2m,迭代完成,此时 m = log ⁡ 2 n m=\log_2n m=log2n
因此, T ( n ) = n T [ 1 ] + n log ⁡ 2 n = n + n log ⁡ 2 n \mathcal T(n)=n\mathcal T[1]+n\log_2n=n+n\log_2n T(n)=nT[1]+nlog2n=n+nlog2n。当 n ≥ 2 n\geq2 n2时, n ≤ log ⁡ 2 n n\leq\log_2n nlog2n,因此最优时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

最坏时间复杂度
快排最坏的情况是每次选取数组的最大或最小元素作为哨兵元素,此时退化为冒泡排序(一次仅排好一个元素)。
时间复杂度 T [ n ] = n ∗ ( n − 1 ) = n 2 + n \mathcal T[n]=n*(n-1)=n^2+n T[n]=n(n1)=n2+n,即最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)

空间复杂度
就地快排的额外空间(存储哨兵元素)为 O ( 1 ) O(1) O(1),而真正需要考虑的是维护递归调用所需的存储空间。最优情况是每次平分数组,递归调用 log ⁡ n \log n logn次,空间复杂度为 O ( log ⁡ n ) O(\log n) O(logn);最坏情况是冒泡排序,递归调用 n n n次,空间复杂度为 O ( n ) O(n) O(n)

5.4 动态演示

5.5 python实现

def quick_sort(array, start, end, key=lambda x: x):
    """快速排序"""
    if start >= end:
        return array

    i, j, value = start, end, array[start]
    while i < j:
        while i < j and key(value) <= key(array[j]):
            j -= 1
        if i < j:
            array[i] = array[j]
            i += 1

        while i < j and key(value) >= key(array[i]):
            i += 1
        if i < j:
            array[j] = array[i]
            j -= 1

    array[i] = value
    quick_sort(array, start, i - 1)
    quick_sort(array, j + 1, end)


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    quick_sort(array, 0, 19)
    print(array)

6、归并排序(Merge Sort)

6.1 基本思想
利用 分而治之 的思想,先将序列分成有序的子序列(1个元素的序列为有序序列),再将有序子序列两两合并为完全有序序列,最后复制回原序列。

数据结构与算法:常见排序算法及其python实现_第2张图片
图1 归并排序的分、治思想

6.2 算法实现
若数组a的[start, mid]和[mid+1, end]段分别存储两个有序子序列,则合并的思路:
(1) 令i、j分别为两个子序列的首元素位置,若a[i] <= a[j],则将a[i]元素添加至缓存数组并令i加1,否则将a[j]添加至缓存数组并令j加1,循环上述过程直至其中一个子序列中的元素全部添加至缓存数组;
(2) 将另一个的子序列(未遍历的子序列)中剩余的元素依次添加至缓存数组,最后将数组a[start, end]中的替换为缓存数组中的元素;

6.3 算法分析
时间复杂度: 一次归并平均比较n次,归并总次数 log ⁡ 2 ( n ) \log_2(n) log2(n)(完全二叉树深度),时间复杂度为 O ( n log ⁡ ( n ) ) O(n\log(n)) O(nlog(n))
空间复杂度: 需与原始数组等长的临时空间,空间复杂度为 O ( n ) O(n) O(n)

6.4 动态演示

6.5 python实现

def __merge(arr, start, mid, end):
    """合并有序子序列"""
    cache = []
    i, j = start, mid + 1
    while i <= mid and j <= end:
        if arr[i] <= arr[j]:
            cache.append(arr[i])
            i += 1
        else:
            cache.append(arr[j])
            j += 1
    if i <= mid:
        cache.extend(arr[i:mid + 1])
    else:
        cache.extend(arr[j:end + 1])
    arr[start:end + 1] = cache


def merge_sort(arr, start, end):
    """归并排序"""
    if not arr or start >= end:
        return
    mid = start + end >> 1
    merge_sort(arr, start, mid)
    merge_sort(arr, mid + 1, end)
    __merge(arr, start, mid, end)


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    merge_sort(array, 0, 19)
    print(array)

7、堆排序(Heap Sort)——选择排序升级

7.1 基本思想
将待排序数组看作为一个完全二叉树的顺序结构,调整父子节点的顺序,使子节点的值不大于父节点的值,以建立起最大堆结构(数组堆化)。建立最大堆后,逐次交换数组首尾元素(最大值放到最后),并在每次交换后重新建立起去除末尾叶节点的完全二叉树(数组)为最大堆,如此往复直至堆(数组)中仅含一个元素。

7.2 算法实现
堆排序算法实现:
(1) 从最大编号的非叶节点开始直到根节点(序列首元素),依次调整各节点所在的树为 最大堆结构,得到堆化的序列;
(2) 将堆顶元素(最大元素)和末尾元素交换,并重新调整去除末尾元素的序列为最大堆;
(3) 重复步骤2,直至堆中仅有一个元素;

7.3 算法分析
初始堆时间复杂度
假设堆的高度为 k k k i i i为当前节点所在层。由于 i i i层的节点数为 2 i − 1 2^{i-1} 2i1 i i i层节点最多交换 k − i k-i ki次且最后一层叶节点不需要比较,因此第 i i i层的复杂度为 ( k − i ) × 2 i − 1 (k-i)\times2^{i-1} (ki)×2i1 i ∈ [ 1 , k − 1 ] i\in[1, k-1] i[1,k1],故总时间复杂度为
S = 1 × 2 k − 2 + 2 × 2 k − 3 + ⋯ + ( k − 1 ) × 2 0 S =1\times2^{k-2} + 2\times2^{k-3}+\cdots+(k-1)\times2^0 S=1×2k2+2×2k3++(k1)×20

上述等式两边乘以2再减去上式得
S = 2 k − 1 + 2 k − 2 + ⋯ + 2 1 − k + 1 = 2 k − k − 1 S = 2^{k-1}+2^{k-2}+\cdots+2^1-k+1=2^k-k-1 S=2k1+2k2++21k+1=2kk1

带入 k = log ⁡ 2 n k=\log_2n k=log2n,得 S = n − log ⁡ n − 1 S = n-\log n -1 S=nlogn1,故时间复杂度为 O ( n ) O(n) O(n)

重建堆复杂度
每次交换堆顶元素与末尾叶节点,因此每次均从顶部向下重建堆,时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

综上所述,重建堆 n − 1 n-1 n1次,故时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

7.4 动态演示

7.5 python实现

def max_heapfiy(arr, start, end):
    """大堆化"""
    son = 2 * start + 1
    while son <= end:
        if son < end and arr[son] < arr[son + 1]:
            son += 1
        if arr[start] >= arr[son]:
            return
        arr[start], arr[son] = arr[son], arr[start]
        start, son = son, 2 * son + 1


def heap_sort(arr):
    """堆排序"""
    end = len(arr) - 1
    for start in range((len(arr) >> 1) - 1, -1, -1):
        max_heapfiy(arr, start, end)

    for i in range(end, 0, -1):
        arr[0], arr[i] = arr[i], arr[0]
        max_heapfiy(arr, 0, i - 1)


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    heap_sort(array)
    print(array)

8、计数排序(Counting Sort)

8.1 基本思想
使用 一次分配与收集 过程 ,实现非比较式排序。分配是将数据转换为键,并存储在辅助空间键指示的位置,或者统计数据的频数,再经过转换获得有序信息。收集是将辅助空间中的数据复制回数组,或者根据有序信息将数据分别存储在特定位置。

8.2 算法实现
假设数组array=[9, 7, 6, 3, 9, 2, 7, 3, 2, 8],有序后为[2, 2, 3, 3, 6, 7, 7, 8, 9]。

使用数组的算法实现:
(1) 计算数组最大值max和最小值min,分配长度为max - min + 1、初值为0的频数表(数组);
(2) 遍历待排序数组,统计各数据num的频数 (分配过程),其中num的频数在频数表下标num - min处;

频数表:

下标 0 1 2 3 4 5 6 7
频数 2 2 0 0 1 2 1 2

(3)将各数据频数依次累加,得到索引表,索引表的键值key-value表示数据key+min在有序序列下标value-1处;
\quad 如键值4-5表示最后一个数据6在有序序列下标4处;

索引表:

下标 0 1 2 3 4 5 6 7
索引 2 4 4 4 5 7 8 10

(4) 分配与原数组等长的缓存数组,从后向前遍历原数组(稳定排序),将数据num转换为键(num - min)并查询索引表中的对应值value,然后将数据存储至缓存数组下标value-1处 (收集过程),同时将索引值value减1(下次相同数据将放在前一位置);
\quad 如首先扫描到8,应存储至缓存数组下标7处;接着扫描到2,应存储至缓存数组小标1处,并将索引值1;

添加值后的缓存数组:

下标 0 1 2 3 4 5 6 7 8 9
数据 2 8

更新后的索引表:

下标 0 1 2 3 4 5 6 7
索引 1 4 4 4 5 7 7 10

使用链表的算法实现:
(1) 计算数组元素的最大值max和最小值min,分配编号为0~max-min的max - min + 1个链表;
(2) 遍历待排序数组,将数据num 分配 到编号为num-min的链表;
(3) 逐个 收集 各链表中的元素至原数组;

8.3 算法分析
计数排序适用于 数值分布稠密、跨度小的整数排序。若序列中存在负整数需将序列整体加上一个正整数,使最小数非负。

若待排序数组的长度为n取值范围为m,则
时间复杂度: 分配数据复杂度 O ( n ) O(n) O(n)、收集数据复杂度 O ( m ) O(m) O(m),总时间复杂度为 O ( n + m ) O(n+m) O(n+m)
空间复杂度: 频数表/链表占用 O ( m ) O(m) O(m)、缓存数据占用 O ( n ) O(n) O(n),总空间复杂度为 O ( n + m ) O(n+m) O(n+m)

8.4 动态演示——使用链表

8.5 python实现

def min_max(arr):
    """查找数组中最小、最大值"""
    min = max = arr[0]
    for num in arr[1:]:
        if num > max:
            max = num
        elif num < min:
            min = num
    return min, max


def counting_sort1(arr):
    """计数排序——使用数组"""
    min, max = min_max(arr)
    k, n = max - min + 1, len(arr)

    count = [0] * k
    for num in arr:
        count[num - min] += 1

    for i in range(1, k):
        count[i] += count[i - 1]

    cache = [0] * n
    for num in arr[::-1]:
        count[num - min] -= 1
        cache[count[num - min]] = num

    for i in range(n):
        arr[i] = cache[i]


def counting_sort2(arr):
    """计数排序——使用链表"""
    min, max = min_max(arr)
    k, n = max - min + 1, len(arr)

    count = [[] for _ in range(k)]
    for num in arr:
        count[num - min].append(num)

    sorted_index = 0
    for seq in count:
        for num in seq:
            arr[sorted_index] = num
            sorted_index += 1


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    counting_sort1(array)
    print(array)

    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    counting_sort2(array)
    print(array)

9、基数排序(Radix Sort)

9.1 基本思想
使用 多次分配与收集 过程 (多关键字),实现非比较式排序。基数指的是数的位,如整数的个位、十位和百位等。

基数排序分为两种:最低位数字LSD、最高位数字MSD。
Least Significant Digit(LSD):短的关键字较小,如2 < 10、“aa” > “b”;
Most Significant Digit(MSD):按字典顺序排序,如2 > 10、“aa” < “b”;

9.2 LSD算法实现
若基数为10,对数据长度为n、最大数的位数为d的整数数组进行排序。

使用数组的算法实现:
(1) 初始时,分配长度为n的缓存数组
(2) 分配长度为10的频数表(初值为0),遍历待排序数组,统计各数据个位数的频数;
(3) 将各频数依次累加,得到索引表,索引表的值value表示以当前键为个位数的数据在有序序列下标value-1处;
(4) 从后向前遍历原数组(稳定排序),根据数据的个位数值(与索引表的键对应)查询索引表中对应值value,然后将数据存储至缓存数组下标value-1处 (收集过程),同时将索引值value减1(下次相同数据将放在前一位置);
(5) 将缓存数组中的数据复制回原数组,并对原数组的十位、百位等基数重复步骤2-5,直至所有基数分配收集完成;

使用链表的算法实现:
(1) 初始时,分配编号为0~9的10个链表;
(2) 遍历待排序数组,将个位数为num的数据分配给编号为num的链表;
(3) 逐个收集并释放各链表中的数据,将所得数据依次覆盖原数组;
(5) 对原数组的十位、百位等基数重复步骤2-5,直至所有基数分配收集完成;

9.3 算法分析
基数排序适用于 数据非负且取值范围较小 的序列排序。
若待排序数据的数据类型为整数,长度为n,最大数的位数为d,基数为m(整数一般为10),则

时间复杂度: 一次分配的复杂度为 O ( n ) O(n) O(n),一次收集的复杂度为 O ( m ) O(m) O(m),需分配收集 d d d次,总时间复杂度为 O ( d ( n + m ) ) O(d(n+m)) O(d(n+m))
空间复杂度: 频数表或链表占用 O ( m ) O(m) O(m)、缓存数据占用 O ( n ) O(n) O(n),总空间复杂度为 O ( n + m ) O(n+m) O(n+m)

9.4 动态演示——使用链表
数据结构与算法:常见排序算法及其python实现_第3张图片

9.5 python实现

def radix_sort1(arr):
    """基数排序——使用数组"""
    max_val = max(arr)
    radix = 1
    n = len(arr)
    cache = [0] * n

    while max_val / radix > 1:
        digit_count = [0] * 10
        for num in arr:
            digit_count[int(num / radix) % 10] += 1
        for i in range(1, 10):
            digit_count[i] += digit_count[i - 1]

        for num in arr[::-1]:
            pos = int(num / radix) % 10
            digit_count[pos] -= 1
            cache[digit_count[pos]] = num

        for i in range(n):
            arr[i] = cache[i]

        radix *= 10


def radix_sort2(arr):
    """基数排序——使用链表"""
    max_val = max(arr)
    radix = 1
    cache = [[] for _ in range(10)]
    while max_val / radix > 1:
        for num in arr:
            cache[int(num / radix) % 10].append(num)

        sorted_index = 0
        for seq in cache:
            while seq:
                arr[sorted_index] = seq.pop(0)
                sorted_index += 1

        radix *= 10


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    radix_sort1(array)
    print(array)

    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    radix_sort2(array)
    print(array)

10、桶/箱排序(Bucket Sort)

10.1 基本思想
假设序列中数据服从均匀分布(等可能地落在等间隔的值区间内),将序列通过映射分配至有限桶(每桶存放指定区间内的数据),再对每桶分别排序(插入排序等),最后拼接各桶中的有序序列,即得到整体有序序列。使用一次分配与收集过程,典型的分而治之思想。

10.2 算法实现
(1) 计算待排序序列的取值区间,根据设定的桶数m,均匀划分取值区间为m个区间并对应到桶(链表);
(2) 遍历待排序序列,将数据根据其所在区间分配给对应桶;
(3) 对非空桶进行内部排序,排序算法可使用插入排序、快排等;
(4) 按顺序遍历各桶,依次将各桶中的数据从底至上添加回原序列;

10.3 算法分析
桶排序 整体的稳定性取决于各桶内部排序方法,如内部使用快速排序则整体为不稳定排序,使用插入排序则整体为稳定排序。
桶排序对数据取值区间没有要求,但要求数据分布均匀。若所有数据被分配到一个桶,则退化为一般排序算法。

时间复杂度
对于含n个待排数据的数组,分配m个桶,则平均每桶分配n/m个数据,则桶排序的平均复杂度(单桶的排序使用快排等)
O ( n + m n m log ⁡ ( n m ) ) = O ( n + n ( log ⁡ n − log ⁡ m ) ) O(n+m\frac{n}{m}\log(\frac{n}{m}))=O(n+n(\log n - \log m)) O(n+mmnlog(mn))=O(n+n(lognlogm))

n n n接近于 m m m时,时间复杂度趋近于 O ( n ) O(n) O(n);当所有数据落在一个桶中,就退化为一般排序,时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)

空间复杂度
需与数据等长的空间 n n n,以及m个桶(链表),空间复杂度为 O ( n + m ) O(n+m) O(n+m)

10.4 计数排序、基数排序与桶排序的比较
三种排序均是非比较式排序,以 空间换时间,最优时间复杂度为线性级别。

名称 计数排序,k为数值范围 基数排序,m为基数,d为位数 桶排序,m为桶数
时间复杂度 O ( n + k ) O(n+k) O(n+k) O ( d ( n + m ) ) O(d(n+m)) O(d(n+m)) O ( n + n ( log ⁡ n − log ⁡ m ) ) O(n+n(\log n - \log m)) O(n+n(lognlogm))
空间复杂度 O ( n + k ) O(n+k) O(n+k) O ( n + m ) O(n+m) O(n+m) O ( n + m ) O(n+m) O(n+m)

计数排序和基数排序可看作为特殊的桶排序,如
当桶排序中的桶数为数值取值范围,则桶排序变为计数排序;
基数排序可看做多次桶排序(计数排序),在每一个数位上执行一次桶排序;
当取数值的最大值作为基数,则基数排序变为计数排序;

计数排序适用于数据集中、取值范围小的整数序列排序;基数排序适用于数值非负、数据集中且取值范围小的序列排序;桶排序适用于数据分布均匀的序列排序。

10.5 python实现

def min_max(arr):
    """查找数组中最小、最大值"""
    min = max = arr[0]
    for num in arr[1:]:
        if num > max:
            max = num
        elif num < min:
            min = num
    return min, max


def insertion_sort(arr):
    """插入排序"""
    for i in range(1, len(arr)):
        j, value = i - 1, arr[i]
        while j >= 0 and value < arr[j]:
            arr[j + 1] = arr[j]
            j -= 1
        arr[j + 1] = value


# 默认桶数
BUCKET_SIZE = 10


def bucket_sort(arr, bucket_size=BUCKET_SIZE):
    """桶排序"""
    min, max = min_max(arr)
    bucket_count = int((max - min) / bucket_size) + 1
    buckets = [[] for _ in range(bucket_size)]
    for num in arr:
        buckets[int((num - min) / bucket_count)].append(num)

    sorted_index = 0
    for bucket in buckets:
        if bucket:
            insertion_sort(bucket)
            for num in bucket:
                arr[sorted_index] = num
                sorted_index += 1


if __name__ == '__main__':
    array = [19, 8, 15, 2, 16, 17, 1, 9, 13, 14, 6, 3, 12, 18, 7, 0, 5, 10, 4, 11]
    bucket_sort(array)
    print(array)

你可能感兴趣的:(数据结构与算法)