计数排序算法详解 - 从原理到实现
计数排序概述
计数排序是一种非比较型的线性时间排序算法,特别适用于整数数据的排序场景。与常见的基于比较的排序算法(如快速排序、归并排序)不同,计数排序通过统计元素出现次数来实现排序,这使得它在特定条件下可以达到O(n)的时间复杂度。
算法核心思想
计数排序的基本原理可以概括为以下三个步骤:
- 统计频次:遍历数组,统计每个元素出现的次数
- 计算位置:根据统计结果计算每个元素在排序后数组中的最终位置
- 重构数组:按照计算得到的位置信息,将元素放置到正确位置
这种思想充分利用了数组索引的有序性,通过将元素值映射到数组索引来实现排序。
算法详细步骤
1. 确定数据范围
首先需要确定待排序数组中元素的最小值nums_min
和最大值nums_max
,计算出数据范围:
size = nums_max - nums_min + 1
这个范围决定了我们需要多大的计数数组来统计所有可能的元素值。
2. 初始化计数数组
创建一个大小为size
的计数数组counts
,初始化为全0:
counts = [0 for _ in range(size)]
3. 统计元素频次
遍历原始数组,统计每个元素出现的次数:
for num in nums:
counts[num - nums_min] += 1
这里使用num - nums_min
作为索引,将元素值映射到从0开始的连续索引。
4. 计算累积频次
将计数数组转换为累积计数数组,表示每个元素在排序后数组中的最后位置:
for i in range(1, size):
counts[i] += counts[i - 1]
5. 构建排序结果
逆序遍历原始数组,根据累积计数数组将元素放置到正确位置:
res = [0 for _ in range(len(nums))]
for i in range(len(nums) - 1, -1, -1):
num = nums[i]
res[counts[num - nums_min] - 1] = num
counts[num - nums_min] -= 1
逆序遍历保证了排序的稳定性,即相同元素的相对顺序保持不变。
算法特性分析
时间复杂度
计数排序的时间复杂度为O(n + k),其中:
- n是待排序数组的长度
- k是数据的范围大小(最大值与最小值的差加1)
当k与n同数量级时,时间复杂度可视为线性O(n)。
空间复杂度
计数排序需要额外的O(k)空间来存储计数数组。当数据范围很大时,这会消耗较多内存。
稳定性
计数排序是一种稳定的排序算法,因为逆序遍历和放置保证了相同元素的原始相对顺序。
适用场景
计数排序最适合以下场景:
- 待排序数据为整数
- 数据范围不大(k值较小)
- 需要稳定排序
- 数据分布相对集中
实际应用示例
假设我们有数组[3, 0, 4, 2, 5, 1, 3, 1, 4, 5]
,让我们看看计数排序如何处理它:
- 确定范围:最小值为0,最大值为5,范围size=6
- 统计频次:counts = [1, 2, 1, 2, 2, 2]
- 累积计数:counts = [1, 3, 4, 6, 8, 10]
- 逆序填充:
- 最后一个元素5:放在位置9(counts[5]=10-1=9)
- 倒数第二个元素4:放在位置7(counts[4]=8-1=7)
- ...依此类推
最终得到排序结果[0, 1, 1, 2, 3, 3, 4, 4, 5, 5]
算法优化与变种
简化版本
对于不需要稳定排序的场景,可以省略累积计数步骤,直接根据频次重建数组:
res = []
for num in range(nums_min, nums_max + 1):
res.extend([num] * counts[num - nums_min])
这种实现更简单,但失去了稳定性。
处理负数
基本实现已经可以处理负数,因为nums_min
可能是负数,计算索引时会自动调整。
大范围数据
对于数据范围很大的情况,可以考虑以下优化:
- 分段处理:将大范围分成多个小范围分别排序
- 基数排序:结合计数排序实现多轮排序
总结
计数排序是一种高效的线性时间排序算法,特别适合整数排序且数据范围不大的场景。它的主要优势在于时间复杂度低且稳定,但需要额外的空间开销。理解计数排序的原理有助于我们更好地掌握非比较排序的思想,也为学习更复杂的基数排序等算法打下基础。
在实际应用中,当数据满足计数排序的前提条件时,它可以提供比传统比较排序更好的性能表现。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考