按频率表求中位数:用累计频数定位
LeetCode 571 · 中位数、累计求和、频率展开
面试怎么回答
按数值排序算累计频数,每个数值占据(前缀和 - 频数, 前缀和] 这一段位置。总数的一半落在哪一段,那个数值就是中位数;总数为偶数且一半正好在分界上时会命中两个数值,取平均。整个过程不需要把频率表展开。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
题意
表里存的是「数值 + 出现次数」,而不是展开后的明细。求所有数字(按次数展开后)的中位数。
表结构
- Numbers:Number(主键)、Frequency
思路
不用真的展开。按数值排序算累计频数:某个数值覆盖的位置区间是(之前的累计数, 之前的累计数 + 自身频数]。中位数位置是总数的一半附近,只要某个数值的区间覆盖了总数/2,它就是中位数(或中位数之一),对命中的数值取平均即可。
参考解法
写法 1:窗口累计频数,区间覆盖总数一半的数值就是中位数
select
avg(cast(number as float)) median
from
(
select
Number,
Frequency,
sum(Frequency) over(order by Number) - Frequency prev_sum,
sum(Frequency) over(order by Number) curr_sum
from Numbers
) t1,
(
select
sum(Frequency) total_sum
from Numbers
) t2
where
t1.prev_sum <= (cast(t2.total_sum as float) / 2) and
t1.curr_sum >= (cast(t2.total_sum as float) / 2)
写法 2:自连接计算累计频数,适合不支持窗口函数的环境
SELECT
AVG(Number)median
FROM
(SELECT n1.Number FROM Numbers n1 JOIN Numbers n2 ON n1.Number>=n2.Number
GROUP BY
n1.Number
HAVING
SUM(n2.Frequency)>=(SELECT SUM(Frequency) FROM Numbers)/2
AND
SUM(n2.Frequency)-AVG(n1.Frequency)<=(SELECT SUM(Frequency) FROM Numbers)/2
)s
易错点
- 真的把频数展开成明细会让数据量爆炸。
- MySQL 8.0.17 之前不支持 CAST(... AS FLOAT),可以改成乘以 1.0 或 CAST AS DECIMAL。