19. 数组的众数与中位数
数组的众数和中位数,这两个概念听着挺数学,其实在工程里特别实用。我当年做数据采集系统时,传感器传回来的数据经常有毛刺,这时候中位数滤波就派上用场了。而众数呢,说白了就是找「出现最多的那个数」——比如统计用户最常用的功能ID,或者找出系统日志里最常见的错误码。
这一节,咱们就手把手把这两个算法写出来。别怕,代码量不大,但思路得理清。
19.1 什么是众数?什么是中位数?
先简单回顾一下定义:
- 众数:数组中出现次数最多的元素。注意,可能有多个众数(比如 [1,1,2,2] 中 1 和 2 都是众数)。
- 中位数:将数组排序后,位于中间位置的数。如果数组长度是奇数,中位数就是中间那个;如果是偶数,中位数通常是中间两个数的平均值。
举个例子:
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// 众数:5(出现了3次)
// 排序后:1,1,2,3,3,4,5,5,5,6,9 → 中位数:4
嗯,这里要注意:中位数要求排序,众数不要求排序,但需要统计频率。
19.2 寻找众数——计数法
找众数最直接的方法就是「统计每个数出现的次数」。我习惯用一个辅助数组来计数,前提是数据范围已知且不大。如果数据范围很大或者不确定,那就得用哈希表了——不过咱们先讲基础版。
19.2.1 基础版:已知数据范围
假设数组元素都在 0~100 之间,那我们可以开一个大小为 101 的计数数组。
#include <stdio.h>
#define MAX_VAL 100
int findMode(int arr[], int n) {
int count[MAX_VAL + 1] = {0};
int i, mode = arr[0], maxCount = 0;
for (i = 0; i < n; i++) {
count[arr[i]]++;
if (count[arr[i]] > maxCount) {
maxCount = count[arr[i]];
mode = arr[i];
}
}
return mode;
}
int main() {
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("众数是:%d\n", findMode(arr, n));
return 0;
}
这段代码有个小问题:它只返回第一个遇到的众数。如果多个数出现次数相同,它只会返回最先达到最大次数的那个。我在项目中遇到过这种情况——统计用户操作时,发现两个操作频率一样高,结果只报了一个,排查了半天才发现是算法没处理多众数。
19.2.2 进阶版:处理多众数
如果你想找出所有众数,可以分两步走:先找出最大出现次数,再遍历一遍找出所有达到这个次数的元素。
void findAllModes(int arr[], int n) {
int count[MAX_VAL + 1] = {0};
int i, maxCount = 0;
// 第一遍:统计频率,找出最大次数
for (i = 0; i < n; i++) {
count[arr[i]]++;
if (count[arr[i]] > maxCount) {
maxCount = count[arr[i]];
}
}
// 第二遍:输出所有众数
printf("众数(出现 %d 次):", maxCount);
for (i = 0; i <= MAX_VAL; i++) {
if (count[i] == maxCount) {
printf("%d ", i);
}
}
printf("\n");
}
你看,这样就不会漏掉了。不过要记住,第二遍遍历的范围是 0~MAX_VAL,如果数据稀疏,效率会有点低。你想想看,如果数组里只有 3 个数,但范围是 0~10000,那遍历 10001 次就太浪费了。
19.3 寻找中位数——排序法
中位数的核心就是排序。我建议用 qsort,它是 C 标准库自带的快速排序,效率高,代码也简洁。
19.3.1 标准实现
#include <stdio.h>
#include <stdlib.h>
int cmp(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
double findMedian(int arr[], int n) {
qsort(arr, n, sizeof(int), cmp);
if (n % 2 == 1) {
// 奇数个元素
return arr[n / 2];
} else {
// 偶数个元素,取中间两个的平均值
return (arr[n / 2 - 1] + arr[n / 2]) / 2.0;
}
}
int main() {
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("中位数是:%.1f\n", findMedian(arr, n));
return 0;
}
这里有个细节:偶数长度时,我返回的是两个中间数的平均值。但有些场景下,你可能想要「下中位数」或「上中位数」——比如在二分查找中,我们经常取左中位数。具体用哪个,得看业务需求。
memcpy 复制数组,然后对副本排序,这样原数组不受影响。
19.4 众数与中位数的应用场景
光讲理论没意思,我分享几个实际用过的场景:
| 场景 | 用众数 | 用中位数 |
|---|---|---|
| 传感器数据去噪 | 找出最常见的读数,忽略偶发毛刺 | 滑动窗口中位数滤波,平滑曲线 |
| 用户行为分析 | 统计最常点击的按钮 | 分析用户操作时间的中间值 |
| 成绩统计 | 找出最常见的分数段 | 计算班级成绩的中位数,避免极端分影响 |
| 网络延迟监控 | 找出最常见的延迟值 | 用中位数代替平均值,抗干扰能力强 |
我曾经做过一个网络延迟监控工具,刚开始用平均值来统计,结果偶尔一次超时就把平均值拉得很高,导致误报。后来改成中位数,效果立竿见影——因为中位数对异常值不敏感,更能反映「大多数情况下的表现」。
19.5 知识体系图
下面这张图把本章的核心逻辑串起来了。你看,众数和中位数虽然都是统计量,但实现思路完全不同:一个靠计数,一个靠排序。
19.6 避坑指南
最后,我把自己踩过的坑总结一下,你写代码时留个心眼:
- 数组越界:计数法里,如果元素值超出了计数数组的范围,程序会直接崩溃。我曾经在嵌入式设备上遇到过,数组越界导致系统重启,排查了一整天才发现是传感器偶尔输出 -1 导致的。
- 浮点数精度:中位数是偶数长度时,平均值可能不是整数。用
2.0而不是2做除法,否则整数除法会截断小数。 - 空数组:如果数组长度为 0,众数和中位数都没有意义。写代码时记得加个判断,返回一个错误值或者直接报错。
- 排序副作用:
qsort会改变原数组顺序。如果你后面还要用原数组,记得先拷贝一份。
嗯,众数和中位数就讲到这里。这两个算法虽然基础,但用好了能解决很多实际问题。下次你处理数据时,不妨想想:我是要找「最常见的」,还是找「最中间的」?选对了工具,事半功倍。
公众号:蓝海资料掘金营,微信deep3321