【数据结构与算法】———非比较排序

172 阅读8分钟

非比较排序算法:原理、实现与力扣热题解析

在排序算法的大家族中,非比较排序以其独特的优势占据着重要地位。与快速排序、归并排序等比较排序不同,非比较排序不依赖元素间的比较操作,而是通过利用元素本身的特性(如数值范围、位数分布等)实现排序。这类算法的时间复杂度可以突破 O (n log n) 的理论下限,在特定场景下展现出极高的效率。本文将深入解析基数排序、计数排序和桶排序三种经典非比较排序算法,结合 Python 实现代码,并通过力扣热题分析其实际应用。

计数排序:整数排序的高效方案

计数排序是一种针对整数序列的排序算法,其核心思想是通过统计每个元素出现的次数来确定元素在排序结果中的位置。该算法适用于元素值范围相对较小的场景,当最大值与最小值的差值(记为 m)远小于元素数量(n)时,能达到 O (n + m) 的时间复杂度。

算法原理

计数排序的工作流程可分为三个步骤:

  1. 统计频率:遍历待排序数组,记录每个元素出现的次数
  1. 计算前缀和:将频率数组转换为前缀和数组,确定每个元素在结果中的起始位置
  1. 构建结果:反向遍历原数组,根据前缀和数组放置元素并更新位置指针

Python 实现

def counting_sort(nums):
    if not nums:
        return []
    
    # 确定数值范围
    min_val = min(nums)
    max_val = max(nums)
    range_size = max_val - min_val + 1
    
    # 初始化计数数组和结果数组
    count = [0] * range_size
    result = [0] * len(nums)
    
    # 统计每个元素的出现次数
    for num in nums:
        count[num - min_val] += 1
    
    # 计算前缀和,确定元素位置
    for i in range(1, len(count)):
        count[i] += count[i - 1]
    
    # 反向遍历构建结果(保持稳定性)
    for num in reversed(nums):
        index = count[num - min_val] - 1
        result[index] = num
        count[num - min_val] -= 1
    
    return result

力扣热题应用:排序数组(LeetCode 912)

在处理元素范围有限的整数排序问题时,计数排序展现出显著优势。例如 LeetCode 912 题要求对整数数组进行排序,当测试用例中元素集中在较小范围时,计数排序的性能远超比较排序。

def sortArray(nums):
    # 针对LeetCode测试数据优化的计数排序实现
    if not nums:
        return []
    
    min_val = min(nums)
    max_val = max(nums)
    
    # 当数值范围过大时,切换为内置排序(避免内存溢出)
    if max_val - min_val > 10**6:
        return sorted(nums)
    
    return counting_sort(nums)

该实现通过动态判断数值范围选择排序策略,在保证正确性的同时兼顾了效率与内存消耗。

桶排序:分布式排序的灵活选择

桶排序是一种分布式排序算法,它将待排序元素分配到多个桶中,对每个桶内元素单独排序后,再将所有桶的内容按顺序合并。桶排序的性能高度依赖于桶的设计和元素的分布特性,理想情况下时间复杂度为 O (n)。

算法原理

桶排序的关键步骤包括:

  1. 桶的创建:根据数据范围和分布特性,创建适量的空桶
  1. 元素分配:将元素按照一定规则(通常是映射函数)分配到对应的桶中
  1. 桶内排序:对每个非空桶使用合适的排序算法(可递归使用桶排序)
  1. 结果合并:将所有桶中的元素按顺序拼接,得到排序结果

Python 实现

def bucket_sort(nums, bucket_size=5):
    if len(nums) < 2:
        return nums
    
    # 确定数据范围
    min_val = min(nums)
    max_val = max(nums)
    
    # 计算桶的数量
    bucket_count = (max_val - min_val) // bucket_size + 1
    buckets = [[] for _ in range(bucket_count)]
    
    # 分配元素到桶中
    for num in nums:
        index = (num - min_val) // bucket_size
        buckets[index].append(num)
    
    # 桶内排序并合并结果
    result = []
    for bucket in buckets:
        # 对每个桶使用插入排序(小规模数据高效)
        for i in range(1, len(bucket)):
            key = bucket[i]
            j = i - 1
            while j >= 0 and bucket[j] > key:
                bucket[j + 1] = bucket[j]
                j -= 1
            bucket[j + 1] = key
        result.extend(bucket)
    
    return result

力扣热题应用:最大间距(LeetCode 164)

LeetCode 164 题要求计算排序后数组中相邻元素的最大差值,这是桶排序的经典应用场景。通过合理设计桶的大小(不小于最大可能间距),可以确保最大间距一定出现在不同桶的元素之间,从而避免完全排序的开销。

def maximumGap(nums):
    if len(nums) < 2:
        return 0
    
    min_val = min(nums)
    max_val = max(nums)
    n = len(nums)
    
    # 计算桶大小和数量
    bucket_size = max(1, (max_val - min_val) // (n - 1))
    bucket_count = (max_val - min_val) // bucket_size + 1
    
    # 初始化桶,存储每个桶的最小值和最大值
    buckets = [{'min': None, 'max': None} for _ in range(bucket_count)]
    
    # 填充桶
    for num in nums:
        index = (num - min_val) // bucket_size
        if buckets[index]['min'] is None or num < buckets[index]['min']:
            buckets[index]['min'] = num
        if buckets[index]['max'] is None or num > buckets[index]['max']:
            buckets[index]['max'] = num
    
    # 计算最大间距
    max_gap = 0
    prev_max = buckets[0]['max']
    for i in range(1, bucket_count):
        if buckets[i]['min'] is None:
            continue
        current_gap = buckets[i]['min'] - prev_max
        if current_gap > max_gap:
            max_gap = current_gap
        prev_max = buckets[i]['max']
    
    return max_gap

该实现利用桶排序思想,仅通过记录每个桶的最值就高效计算出最大间距,时间复杂度为 O (n)。

基数排序:基于数字位数的排序策略

基数排序是一种通过按位排序实现整体有序的算法,它从最低有效位到最高有效位(或相反顺序)依次对元素进行排序。基数排序通常基于计数排序或桶排序实现每一轮的按位排序,适用于整数或可转换为固定位数表示的元素。

算法原理

基数排序的工作流程如下:

  1. 确定最大位数:找出数组中最大元素的位数,确定排序轮次
  1. 按位排序:从最低位到最高位,每轮针对当前位进行排序
  1. 合并结果:每轮排序后得到按当前位有序的数组,最终得到完全有序的结果

Python 实现

def radix_sort(nums):
    if not nums:
        return []
    
    # 处理负数:将所有数转为正数,记录符号
    signs = [1 if num >= 0 else -1 for num in nums]
    abs_nums = [abs(num) for num in nums]
    
    # 确定最大位数
    max_num = max(abs_nums)
    max_digits = len(str(max_num))
    
    # 对每一位进行计数排序
    for digit in range(max_digits):
        # 初始化计数数组和临时结果数组
        count = [0] * 10
        temp = [0] * len(abs_nums)
        
        # 统计当前位的数字出现次数
        for num in abs_nums:
            current_digit = (num // (10 ** digit)) % 10
            count[current_digit] += 1
        
        # 计算前缀和
        for i in range(1, 10):
            count[i] += count[i - 1]
        
        # 构建临时结果(从后往前遍历保证稳定性)
        for num in reversed(abs_nums):
            current_digit = (num // (10 ** digit)) % 10
            index = count[current_digit] - 1
            temp[index] = num
            count[current_digit] -= 1
        
        abs_nums = temp
    
    # 恢复符号并返回结果
    return [abs_nums[i] * signs[i] for i in range(len(nums))]

力扣热题应用:有效的字母异位词(LeetCode 242)

虽然字母异位词问题通常使用哈希表解决,但也可以通过基数排序思想高效处理。对两个字符串的字符按 ASCII 值进行基数排序后,比较排序结果是否一致即可判断是否为异位词。

def isAnagram(s: str, t: str) -> bool:
    if len(s) != len(t):
        return False
    
    # 使用基数排序思想对字符进行排序
    def radix_sort_chars(chars):
        # 针对ASCII字符(0-127)进行计数排序
        count = [0] * 128
        for c in chars:
            count[ord(c)] += 1
        
        result = []
        for i in range(128):
            result.extend([chr(i)] * count[i])
        return ''.join(result)
    
    return radix_sort_chars(s) == radix_sort_chars(t)

该实现利用字符的 ASCII 值范围有限(0-127)的特性,通过计数排序(基数排序的一种简化形式)快速完成字符排序,时间复杂度为 O (n)。

三种算法的对比与适用场景

非比较排序算法各有特点,在实际应用中需要根据数据特性选择合适的算法:

算法时间复杂度空间复杂度适用场景优势局限性
计数排序O(n + m)O(n + m)整数序列,值范围较小实现简单,效率高不适用于浮点数和大范围数据
桶排序O(n + k)O(n + k)均匀分布的数据灵活性高,可处理浮点数依赖数据分布,桶设计复杂
基数排序O(d(n + k))O(n + k)整数、字符串等可按位处理的数据不依赖数值范围实现较复杂,不适用于非结构化数据

其中,m 为数值范围,k 为桶数量,d 为最大位数。在选择排序算法时,应综合考虑数据类型、分布特性和范围大小,才能充分发挥非比较排序的优势。

非比较排序算法通过巧妙利用数据本身的特性,突破了比较排序的性能瓶颈,在特定场景下展现出卓越的效率。