非比较型排序

非比较型排序

基数排序原理

排序算法复杂度背景

基于比较的排序算法,理论上无法突破(Ω(n \log n))时间复杂度下界 特殊场景下,不依赖元素间比较、利用元素结构特征,可实现线性时间排序

基数排序核心思想

将元素视为由多个 “位”(digit)组成 按位进行多轮排序,每轮针对一个关键位 依赖稳定的子排序算法,保证先前排好的顺序不被打乱 关键保障:排序的稳定性(相同元素相对顺序不变)

经典示例(十进制整数的基数排序):

按最低有效位(LSB)→ 最高有效位(MSB) 逐轮排序 第一轮:按个位数排序(0-9 分组) 后续轮次:依次按十位数、百位数等排序

实操流程:

初始序列:[329, 457, 657, 839, 436] 第一轮(按个位排序): 按个位 0-9 分组 → [436(6), 457(7), 657(7), 329(9), 839(9)] (注:457 与 657 个位相同,保持原顺序) 第二轮(按十位排序): 按十位 0-9 分组 → [329(2), 436(3), 839(3), 457(5), 657(5)] (注:436 与 839 十位相同,保持第一轮后的顺序) 第三轮(按百位排序): 按百位 0-9 分组 → [329(3), 436(4), 457(4), 657(6), 839(8)] (注:436 与 457 百位相同,保持第二轮后的顺序)

每轮排序通过稳定性保留前序结果,最终实现 “高位决定整体顺序”,完成正确排序。

时间复杂度:

每轮扫一遍序列O(n),总共轮数就是基数位数,设为k。 时间复杂度为O(kn),其中k一般不太大。

基数排序的实现

为了加深对基数排序的理解,我们通过一个编程任务来实践其实现。我们将以整数排序为例,并采用从最低有效位开始的方法对数字进行排序(LSD 基数排序)。

在实现过程中,我们需要编写一个按某个位数进行计数排序的辅助函数,然后在主函数中对每一位循环调用该辅助函数。

用计数排序稳定地对基数排序

每选定一个基数后,需要用O(n)的时间复杂度将当前序列按照这个基数进行排序;或说找到按照当前基数下,当前序列的每一个数要前往的位置 这里用“计数排序”这个算法。 具体做法如下:

  1. 建一个新数组count,记录当前基数的每个不同的数有多少个(计数)
  2. 从前往后遍历序列,取出序列每一个元素在当前基数位置上的值,对应计数值+1(count[number]+=1)
  3. 将count转变为前缀和,这样就得到了每一个基数对应的原始数的最后一个,在新序列中的位置
  4. 从后往前遍历原序列,将每一个数(基数上的数为number)放到对应新序列count[number]-1的位置,并让count[number]-=1。
练习:按位计数排序

作为练习,[170, 45, 75, 90, 802, 24, 2, 66],按个位排序(exp=1)进行计数排序,请写下count数组的计数结果(2.步后)和前缀和结果。并表述在第4步的利用方式,从而得到序列结果。

任务:实现 LSD 基数排序

请实现 radix_sort(arr) 函数,对传入的整数列表进行从小到大的排序。你可以假设所有整数为非负且位数相对固定。为了简化问题,我们以十进制为基数(基数=10)实现,并假定输入整数的最大位数为d(例如10^d数量级)。算法思路如下:

从最低位(个位,exp=1)开始,对数组进行计数排序(Counting Sort),按该位的数值对元素排序。

然后依次向更高一位(exp=10,exp=100,...)进行排序,直到处理完最高位。

代码框架

请在下方代码编辑区完成 counting_sort_by_digit 和 radix_sort 函数的实现。

import random
import time

def counting_sort_by_digit(arr, exp):
    """
    对数组按某个数位进行计数排序(稳定的)。
    参数:
        arr: 要排序的数组
        exp: 位标志,表示当前要按照 (exp位) 上的数值进行排序,
             例如 exp=1 表示按个位排序,exp=10 表示按十位排序。
    返回:
        按指定位排序后的数组副本
    """
    n = len(arr)
    output = [0] * n       # 存放排序结果
    count = [0] * 10       # 计数数组(十进制位范围0-9)
    # TODO: 计算每个数组元素在该位上的值,并计数
    

def radix_sort(arr):
    """
    基数排序(按从低位到高位逐位排序)
    参数:
        arr: 待排序的整数列表
    返回:
        按非递减顺序排序的列表
    """
    #TODO: 利用 counting_sort_by_digit 按位排序数组


#测试与性能分析
def measure(sort_func, data):
    start = time.perf_counter_ns()
    sort_func(data)
    end = time.perf_counter_ns()
    return (end - start) / 10**6  # 转换为毫秒

#生成随机测试数据
sizes = [10000, 50000, 100000]
print("基数排序性能测试:")
for n in sizes:
    data = [random.randrange(0, 1000000) for _ in range(n)]
    t = measure(radix_sort, data)
    print(f"数据规模 n={n}: 排序耗时 {t:.2f} ms")

性能讨论

在完成代码并运行测试后,可以看到基数排序在不同规模输入下运行时间增长相对温和。理想情况下,对于固定位数d的整数,基数排序的时间复杂度为O(d \times (n + k)),其中k为每位可能的取值数量(对十进制整数而言k=10)。
d视作常数时,复杂度近似O(n),因此输入规模扩大倍数时,运行时间应近似线性增长。
然而需注意,基数排序的实际常数因子不小(多次稳定排序和额外空间),对中等规模数据Python实现未必比C语言内建排序更快。
更重要的是,基数排序并非万能:如果元素位数dn增长(例如排序1N范围的数,N约为n量级),则d = O(\log n),此时基数排序复杂度≈O(n \log n),并没有打破比较排序下界。因此,基数排序的线性性能依赖于位数相对较小这一前提。

桶排序的原理与实现

桶排序核心定位

典型的线性时间排序算法 利用输入数据的分布特性(假设大致均匀分布在某范围)

桶排序原理与类比

核心逻辑(三步):

第一步:创建若干 “桶”,将元素按值映射到对应桶中(实现粗分类) 第二步:对每个桶内元素单独排序(可选用任意排序算法) 第三步:按桶的顺序合并所有元素,得到有序序列
映射:将小数按值投入对应桶,每个桶仅覆盖原始区间 1/10,数据量少 桶内排序:因元素少,插入排序等简单算法足够快 合并:按桶序号收集,因 “i<j 时,第 i 号桶元素<第 j 号桶元素”,天然有序

案例练习

使用桶排序算法对以下 8 个数字进行排序 [35, 12, 48, 27, 5, 39, 18, 42] 以48为分母,将其他所有数划分到0-0.2-0.4-0.6-0.8-1这5个桶中,输出每个桶里面的数字是什么。

代码实现

请实现 bucket_sort(arr) 函数,将传入的[0,1)区间的小数数组排序。思路如下:
创建若干个空桶(列表),桶的数量可以根据数据规模n选择,这里取n/10(为整数)个桶。
将每个元素按照其值乘以桶数量后的整数部分,映射到对应的桶中。例如值为0.23、桶数为10时,0.23 \times 10 = 2.3,放入索引2号桶。
对每个非空桶内部进行排序。你可以直接使用 Python 内置排序(sorted)或实现简单排序算法。

按桶的顺序依次合并桶内元素,得到排序后的结果。

代码框架
import random
import time

def bucket_sort(arr):
    """
    桶排序(适用于0到1区间的小数)
    参数:
        arr: 介于[0,1)的浮点数列表
    返回:
        升序排序后的列表
    """
    # TODO: 按原理实现桶排序
    n = len(arr)
    if n == 0:
        return arr
    # 1. 创建桶

    # 2. 分配元素到各桶

    # 3. 桶内排序


#简单测试与性能评估
sizes = [1000, 5000, 10000]
print("桶排序性能测试:")
for n in sizes:
    #生成n个[0,1)均匀分布的小数
    data = [random.random() for _ in range(n)]
    start = time.perf_counter_ns()
    sorted_data = bucket_sort(data)
    end = time.perf_counter_ns()
    elapsed = (end - start) / 10**6
    #验证排序正确性
    is_correct = "√" if sorted_data == sorted(data) else "×"
    print(f"数据规模 n={n}: 排序耗时 {elapsed:.2f} ms, 排序正确性 {is_correct}")

桶排序时间复杂度分析

平均情况(理想分布):

前提:n 个元素、m 个桶,数据均匀分布 → 每个桶平均含 n/m 个元素 优化:选 m 接近 n 的量级 → 桶内排序成本视为常数 结果:整体期望运行时间为O(n)(线性时间)

最坏情况(分布不均):

极端场景:所有元素落入同一个桶 复杂度:取决于桶内排序算法(O (n log n) 或 O (n²))+ 分配桶的 O (n) 结果:退化至O (n log n) 甚至更差,失去线性时间优势 结论:理想分布下性能出众,数据分布不均时无优势

桶排序与哈希

核心相通点:映射分散思想

通过函数映射将元素分散到不同 “容器”(桶排序的 “桶”/ 哈希算法的 “哈希桶”),把原问题拆解为小的子问题
桶排序:用映射函数(如 index = int(num * bucket_count))划分元素所属桶,依据元素值分配 哈希算法:用哈希函数计算键的哈希桶索引,确定元素存储位置 这种映射也意味着都对分布均匀性的依赖

关键区别:映射函数的目标差异

对比维度 桶排序的映射函数:一般直接做除法:保留元素大小顺序(按值范围划分) 哈希算法的哈希函数:随机映射函数,实现元素均匀分布,不能用于排序(除非特殊设计有序哈希)

总结

桶排序可看作哈希思想在排序问题中的应用:均通过 “先分类、后处理” 的范式优化性能,但因目标不同,映射函数设计存在本质差异。