数据结构与算法之美 | 排序(3)

线性排序(Linear sort)

桶排序(Bucket sort)

基本思想

  • 将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。
  • 桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
  • 桶排序常常用在外部排序[^1]中。

我们有 10 GB 的订单数据,我们希望按订单金额(假设金额都是正整数)进行排序,但是我们的内存有限,只有几百 MB,没办法一次性把 10 GB 的数据都加载到内存中。这个时候该怎么办?

解决方法

  1. 我们可以先扫描一遍文件,看订单金额所处的数据范围。

    假设经过扫描之后我们得到,订单金额最小是 1 元,最大是 10 万元。

  2. 我们将所有订单根据金额划分到 100 个桶里。

    第一个桶我们存储金额在 1 元到 1000 元之内的订单,第二桶存储金额在 1001 元到 2000 元之内的订单,以此类推。每一个桶对应一个文件,并且按照金额范围的大小顺序编号命名(00,01,02…99)。

  3. 理想的情况下,如果订单金额在 1 到 10 万之间均匀分布,那订单会被均匀划分到 100 个文件中。

    1)每个小文件中存储大约 100 MB 的订单数据,我们就可以将这 100 个小文件依次放到内存中,用快排来排序。

    2)待所有文件都排好序之后,我们只需要按照文件编号,从小到大依次读取每个小文件中的订单数据,并将其写入到一个文件中,那这个文件中存储的就是按照金额从小到大排序的订单数据了。

  4. 非理想情况下,订单按照金额在 1 元到 10 万元之间并不一定是均匀分布的 。

    有可能某个金额区间的数据特别多,划分之后对应的文件就会很大,没法一次性读入内存。

  5. 针对这些划分之后还是比较大的文件,我们可以继续划分。

    比如,订单金额在 1 元到 1000 元之间的比较多,我们就将这个区间继续划分为 10 个小区间,1 元到 100 元,101 元到 200 元,201 元到 300 元…901 元到 1000 元。如果划分之后,101 元到 200 元之间的订单还是太多,无法一次性读入内存,那就继续再划分,直到所有的文件都能读入内存为止。

# bucket sort in Python

import math

def bucket_sort(order_data):
    '''
    使用桶排序算法对订单数据进行排序
    
    参数:
    	order_data(list):待排序列表
    
    返回值:
    	sorted_data(list): 已完成排序列表
    
    '''
    # 找到最大的订单金额
    max_amount = max(order_data)

    # 确定桶的数量
    num_buckets = math.ceil(max_amount / 100)

    # 创建空桶
    buckets = [[] for _ in range(num_buckets)]

    # 将订单数据放入对应的桶中
    for amount in order_data:
        # 划分为100个桶
        bucket_index = amount // 100
        buckets[bucket_index].append(amount)

    # 对每个桶中的数据进行排序
    for bucket in buckets:
        bucket.sort()

    # 合并所有桶的数据
    sorted_data = []
    for bucket in buckets:
        sorted_data.extend(bucket)

    return sorted_data

# 示例订单数据(假设为整数列表)
order_data = [102, 75, 158, 42, 95, 205, 300, 280, 150, 50, 180, 220, 500, 350]

# 使用桶排序进行排序
sorted_orders = bucket_sort(order_data)

# 打印排序结果
print(sorted_orders)

桶排序算法评价

  • 执行效率:最好情况时间复杂度为 O ( n + k ) O(n+k) O(n+k),最坏情况时间复杂度为 O ( n 2 ) O(n^2) O(n2),平均情况时间复杂度为 O ( n ) O(n) O(n)

  • 内存消耗:是一个原地排序算法,空间复杂度为 O ( n + k ) O(n+k) O(n+k)

  • 稳定性:是一个稳定的排序算法


计数排序(Count Sort)

基本思想

  • 计数排序是一种特殊的桶排序

  • 通过计算数组中每个唯一元素的出现次数来对数组的元素进行排序。

  • 计数存储在辅助数组中,排序是通过将计数映射为辅助数组的索引来完成的

高考查分数的时候,系统会显示我们的成绩以及所在省的排名。如果你所在的省有 50 万考生,如何通过成绩快速排序得出名次呢?

解决方法:

  1. 考生的满分是 900 分,最小是 0 分,这个数据的范围很小,所以我们可以分成 901 个桶,对应分数从 0 分到 900 分。
  2. 根据考生的成绩,我们将这 50 万考生划分到这 901 个桶里。桶内的数据都是分数相同的考生,所以并不需要再进行排序。
  3. 我们只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现了 50 万考生的排序。因为只涉及扫描遍历操作,所以时间复杂度是 O(n)。
# count sort in Python

def counting_sort(arr):
    """
    计数排序,arr是数组。
    假设数组中存储的都是非负整数
    
    参数:
    	arr (List[int]): 输入的数组

    返回值:
        None: 原地排序,直接修改输入的数组arr
    """

    n = len(arr)
    if n <= 1:
        return

    # 查找数组中数据的范围
    max_val = arr[0]
    for i in range(1, n):
        if max_val < arr[i]:
            max_val = arr[i]
            
	# 申请一个计数数组count,下标大小[0, max_val]
    count = [0] * (max_val + 1)  

    # 统计每个元素在输入数组中出现的次数
    for i in range(n):
        count[arr[i]] += 1

    # 依次累加
    for i in range(1, max_val + 1):
        # 帮助计算每个元素应该放置在排序结果数组中的正确索引位置
        # count[i] 保存的值表示了输入数组中小于等于 i 的元素的个数
        count[i] = count[i - 1] + count[i]

    # 临时数组result,存储排序之后的结果
    result = [0] * n

    # 从输入数组的最后一个元素开始向前遍历,即从右往左,从后往前遍历。
    for i in range(n - 1, -1, -1):
        index = count[arr[i]] - 1
        result[index] = arr[i]
        count[arr[i]] -= 1

    # 将结果拷贝给arr数组
    for i in range(n):
        arr[i] = result[i]

计数排序算法评价

  • 执行效率:最好情况时间复杂度为 O ( n + k ) O(n+k) O(n+k),最坏情况时间复杂度为 O ( n + k ) O(n+k) O(n+k),平均情况时间复杂度为 O ( n + k ) O(n+k) O(n+k)

  • 内存消耗:是一个原地排序算法,空间复杂度为 O ( N ) O(N) O(N)

  • 稳定性:是一个稳定的排序算法

注意:

  • 计数排序只能用在数据范围不大的场景中,如果数据范围 K 比要排序的数据 n 大很多,就不适合用计数排序了

  • 计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数

基数排序(Radix sort)

基本思想

  1. 首先对具有相同位值的各个数字进行分组来对元素进行排序。
  2. 然后,根据元素的递增/递减顺序对元素进行排序。
# Radix sort in Python

def countingSort(array, place):
    '''
	使用计数排序根据位数对元素进行排序。
	
	参数:
        array (List[int]): 输入的数组。
        place (int): 当前位数。

	返回值:
    	None
	'''
    size = len(array)
    output = [0] * size
    count = [0] * 10

    # # 统计每个数字出现的次数
    for i in range(0, size):
        index = array[i] // place
        count[index % 10] += 1

    # 计算数字累计出现的次数
    for i in range(1, 10):
        count[i] += count[i - 1]

    # 按照位数顺序将元素放置到输出数组中
    i = size - 1
    while i >= 0:
        index = array[i] // place
        output[count[index % 10] - 1] = array[i]
        count[index % 10] -= 1
        i -= 1
        
	# 将输出数组复制回原始数组
    for i in range(0, size):
        array[i] = output[i]



def radixSort(array):
    """
	实现基数排序的主函数。
	
	参数:
    	array (List[int]): 输入的数组。

	返回值:
    	None
	"""
   
    max_element = max(array)

    # 根据数字的位数进行计数排序
    place = 1
    while max_element // place > 0:
        countingSort(array, place)
        place *= 10


data = [121, 432, 564, 23, 1, 45, 788]
radixSort(data)
print(data)

基数排序算法评价

  • 执行效率:最好情况时间复杂度为 O ( n + k ) O(n+k) O(n+k),最坏情况时间复杂度为 O ( n + k ) O(n+k) O(n+k),平均情况时间复杂度为 O ( n + k ) O(n+k) O(n+k)

  • 内存消耗:是一个原地排序算法,空间复杂度为 O ( N ) O(N) O(N)

  • 稳定性:是一个稳定的排序算法

注意

  • 基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。
  • 除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了

参考文献

  • 排序(下):如何用快排思想在O(n)内查找第K大元素?极客时间. 2018
  • https://www.programiz.com/dsa/radix-sort

你可能感兴趣的:(排序算法,桶排序,计数排序,基数排序)