(e)桶/计数排序

官方信息技术老师·16 页·深入(追求细节与边界)·0 次浏览·1 天前
非比较排序线性时间稳定性适用边界

桶排序与计数排序

看完你能讲清桶排序与计数排序的适用边界与稳定性差异

按 空格/→ 演示下一步

1 / 16 页

全部页面点击任意一页,跳回舞台从这页播放

非比较排序线性时间稳定性适用边界

桶排序与计数排序

看完你能讲清桶排序与计数排序的适用边界与稳定性差异

1第 1 页 · 桶排序与计数排序

大数据+小范围的排序问题

现实里有一类棘手问题:数据量极大(百万、千万级),值域却只有几十、几百——比如年龄、考分、星级。在这类场景下,传统的 n log n 比较排序为什么撑不住?

比较排序的理论下限
只靠两两比较的算法,最坏情况逃不开 n log n 次比较
值域 k 是突破口
不比较、直接按值"投放",复杂度可降到 O(n+k)
n 越大 加速越夸张
n=10^6、k=100 时,O(n+k) 比 O(n log n) 快几十倍
百万彩票按尾号分组对应 →大数据小范围排序

尾号只 10 种,清点每种张数即可,无须逐张比较

2第 2 页 · 大数据+小范围的排序问题

问题抽象:映射到小范围

左侧进入原始数据,右侧输出有序序列。映射函数 f 是核心,把大范围 v 压缩为小范围索引 k。

图解渲染中…
b1映射函数 f,如 f(v)=v/10,把 v 压到小范围d1cnt[k]:下标 k 对应的出现次数e1前缀和:算出每个 k 在结果中的起止位置
3第 3 页 · 问题抽象:映射到小范围

桶排序的核心思想

上一页我们把数据映射到了0-100的小范围。映射之后怎么排最自然?按值分桶,桶内排好,再按桶顺序串起来——这就是桶排序的核心三步。

分桶
根据映射规则,把每个元素放进对应的桶
桶内排序
桶通常很小,用插入排序等方法把桶内元素排好
合并
按桶的顺序依次取出所有桶内元素,得到完整序列
邮局按邮编分拣信件对应 →桶排序的三步流程

不同邮编入不同格(分桶),同格按时间排(桶内排序),按格顺序发出(合并)

4第 4 页 · 桶排序的核心思想

桶排序的步骤分解

桶排序分五步:先把数据按值域散入桶,再让每桶内部有序,最后按桶序串回原数组。

1
建桶
根据值域创建若干空桶,建立值到桶号的映射规则
2
分配
遍历原始序列,每个元素按映射落入对应桶
3
桶内排序
对每个非空桶调用排序算法或递归桶排序
4
收集
按桶编号顺序,依次访问并取出每个桶中元素
5
合并
把所有桶取出的元素按序拼接回原数组
5第 5 页 · 桶排序的步骤分解

桶排序完整流程图

沿箭头方向读:从原数组出发,经分桶、桶内排序、最终拼接成有序数组。

图解渲染中…
b1max-min 与桶数 k 的选择直接影响整体效率f1同一桶内可能容纳多个值不同的元素g1桶内常用插入排序,小规模下代价很低
6第 6 页 · 桶排序完整流程图

桶排序代码实现

python

看一段 Python 实现桶排序的核心五步:建桶、入桶、桶内排序、拼接输出。

代码高亮加载中…

五步对应核心思路:找范围→建桶→按比例映射→桶内小排序→顺序拼接。每桶内元素少,整体复杂度近 O(n)。

7第 7 页 · 桶排序代码实现

桶排序复杂度分析

代码写完了,但它到底跑得多快?占多少内存?什么场景才值得用?这一页我们把时间和空间复杂度算清楚,并说清它的适用边界。

时间复杂度 O(n+k)
均匀分布下,n 个元素入桶 O(n),k 桶内排序近似 O(k),合计 O(n+k)
空间复杂度 O(n+k)
k 个桶的容器 + 输入输出数组的额外空间,桶数与数据规模共同决定
适用条件
n 足够大、k 不太大、数据近似均匀分布;三者缺一就可能退化
边界案例
数据严重倾斜时(如全部进同一桶)退化到 O(n²),失去加速意义
快递分拣中心对应 →桶排序复杂度

分拣员(桶内排序)是速度瓶颈;分拣格(桶)太少会堆积,太多多占场地

Tavg(n)=O(n+k),S(n)=O(n+k)T_{\text{avg}}(n) = O(n + k), \quad S(n) = O(n + k)
8第 8 页 · 桶排序复杂度分析

计数排序的核心思想

桶排序把元素分到若干桶再分别排序。但桶内还是要排。如果值域足够小,让每个取值独占一个桶呢?那连比较都不用——数一数就够了。

桶的极端化
每个可能取值独占一个桶,桶的数量等于值域大小
只数不比
遍历输入,遇到 x 就把第 x 个计数器加 1,全程不做任何比较
频次即序
按值从小到大读出每个计数器,就能直接还原完整的有序序列
选票清点对应 →计数排序

每个候选人是固定取值,每张选票是一次出现,票数即为该值的频次

9第 9 页 · 计数排序的核心思想

计数排序的步骤分解

计数排序四步走:建表→计数→定位→落位,把「值→次数→位置→有序」的转换拆开看。

1
建计数数组
按值域[min,max]建长度=max-min+1的count数组并清零
2
统计频次
遍历A[i],对应count[A[i]-min]++,得到每个值出现次数
3
计算位置
对count做前缀和,把「次数」转成「≤该值的总数」即最后落点下标
4
放置元素
反向遍历A,按count定位写入B并count--,反向遍历保证稳定性
10第 10 页 · 计数排序的步骤分解

计数排序完整流程图

看图路径:左侧一列是公共准备,到频次数组C之后分出两条生成有序输出的路线。

图解渲染中…
a5频次数组:C[i]记录值i在A中出现的次数,是本算法核心中间产物a6分叉节点:拿到频次后可走两条不同的输出路线a8反向遍历输入A,保证相同值在输出B中的相对次序不变
11第 11 页 · 计数排序完整流程图

计数排序代码实现

python

稳定版计数排序的 Python 实现,三步走:计数→前缀和→反向填位。

代码高亮加载中…

高亮行串起稳定版计数排序的三大动作:计数累加→前缀和转位置→反向填位。稳定性关键在 reversed(arr):相同值的元素保持原相对顺序。

12第 12 页 · 计数排序代码实现

计数排序复杂度分析

从计数排序的代码看,两层循环一眼就能数出来。但「快」的背后其实有代价——你为速度放弃了空间,还要牺牲一个叫「稳定性」的东西。这个牺牲值不值?这一页讲清楚。

时间复杂度 O(n+r)
两遍扫描:计数 O(n)、累计与还原 O(n+r)。n 是元素数,r 是值域宽度
空间复杂度 O(r)
需要两个长度为 r 的数组:count 和 prefix sum。r 越大越烧内存
稳定性的意义
相同值的元素保持原顺序——这是基数排序能成立的前提条件
适用边界
r 远大于 n 时,计数排序反而比 O(n log n) 慢。值域决定一切
档案按部门再按入职年对应 →稳定排序

同年入职的人要保留部门内原顺序,否则下一轮就排错

13第 13 页 · 计数排序复杂度分析

桶排序 vs 计数排序

两者都'按值分桶',但桶的含义截然不同——一个装区间,一个装具体值,这是最容易混淆的地方。

桶排序
  • 桶:值范围区间(连续)
  • 桶内还需再排序
  • 场景:均匀分布的大数据
计数排序
  • 桶:具体值(离散)
  • 只累加计数,无排序
  • 场景:值域小的离散整数
计数排序是桶排序的极端特例。决策:数据均匀分布 → 桶排序;值域小且离散 → 计数排序。
14第 14 页 · 桶排序 vs 计数排序

算法选择与适用边界

  • 范围紧凑且n较大→优先计数排序
  • 数据近似均匀分布→桶排序更快
  • 计数排序可视为桶尺寸为1的特例
  • 分布严重倾斜时桶排序退化为O(n²)
  • 两者均以O(n+k)空间换取线性时间
延伸主题:基数排序:多关键字的推广比较排序的下界与极限工程中的混合排序策略
15第 15 页 · 算法选择与适用边界

课后思考

先自己想想,再对照参考答案——重点关注思路,不必追求完整。

1为什么桶排序和计数排序能做到 O(n)?它们绕开了什么代价?

参考答案基于比较的排序才有 O(n log n) 下界;它们用『值映射到位置』代替比较,绕开了比较模型,因此不违反下界。

2如果待排数据是 10 亿个取值在 0~9999 的整数,你会选哪种排序?为什么?

参考答案选计数排序。范围只有 10000,计数数组规模远小于数据量,O(n+range)≈O(n),空间开销也值得。

3如果数据范围远大于元素个数 n,计数排序还划算吗?为什么?

参考答案不划算。复杂度退化为 O(n+m)=O(m),数组过大还可能爆内存,此时应回到基于比较的排序。

16第 16 页 · 课后思考