背景

雷达图渲染前要统计采样值的分布——算百分位、做增益和对比度映射,这步一般叫”采样直方图”。打开一个多文件的测线时,每打开一个文件都得算一遍。

原来的 O(N×M)

最早的做法是”对每个待统计的输出桶,把所有采样点扫一遍,数有多少落进这个桶”。N 个采样点 × M 个桶,复杂度 O(N×M)。单文件几百万采样点的时候,光这一步就把打开速度拖慢一大截,多文件叠在一起更明显。

改成 O(N) 单遍桶计数

其实直方图没必要每个桶都扫一遍数据。一趟遍历,把每个采样点直接扔进它对应的桶就行了:

fun histogram(samples: IntArray, min: Int, max: Int, bucketCount: Int): IntArray {
val buckets = IntArray(bucketCount)
val range = (max - min).coerceAtLeast(1)
for (v in samples) {
val idx = ((v - min).toLong() * bucketCount / range).toInt()
.coerceIn(0, bucketCount - 1)
buckets[idx]++
}
return buckets
}

几个细节:rangecoerceAtLeast(1) 兜底,防止 max == min 时除以零;算 idx 时先转 Long 再乘 bucketCount,避免采样值很大时相乘溢出;最后 coerceIn 把正好等于 max 的点按回最后一个桶,防止数组越界。一趟下来 O(N) 完事,桶数 M 不再参与复杂度。

效果

多文件雷达图加载快了很多,这一步从肉眼可见的卡顿变成基本没感觉。而且这是个公共算法,数据处理、层位识别、病害标注这些模块都调它,改一处,全都跟着快了。

直方图、分桶统计这类活,单遍桶计数就够了。以后看到”对每个桶扫一遍数据”的写法,基本都能照着改成单遍 O(N)。我的体会是,先把算法复杂度降下来再去想并发,往往比一上来就堆线程省事。