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 知识体系图

下面这张图把本章的核心逻辑串起来了。你看,众数和中位数虽然都是统计量,但实现思路完全不同:一个靠计数,一个靠排序。

数组的众数与中位数 众数(出现次数最多) 中位数(中间位置的数) 步骤1:遍历数组,统计每个元素出现次数 步骤2:找出最大出现次数 步骤3:输出所有达到最大次数的元素 步骤1:对数组进行排序(qsort) 步骤2:判断数组长度奇偶性 步骤3:取中间元素或中间两数平均值 核心:计数法 vs 排序法

19.6 避坑指南

最后,我把自己踩过的坑总结一下,你写代码时留个心眼:

  • 数组越界:计数法里,如果元素值超出了计数数组的范围,程序会直接崩溃。我曾经在嵌入式设备上遇到过,数组越界导致系统重启,排查了一整天才发现是传感器偶尔输出 -1 导致的。
  • 浮点数精度:中位数是偶数长度时,平均值可能不是整数。用 2.0 而不是 2 做除法,否则整数除法会截断小数。
  • 空数组:如果数组长度为 0,众数和中位数都没有意义。写代码时记得加个判断,返回一个错误值或者直接报错。
  • 排序副作用qsort 会改变原数组顺序。如果你后面还要用原数组,记得先拷贝一份。

嗯,众数和中位数就讲到这里。这两个算法虽然基础,但用好了能解决很多实际问题。下次你处理数据时,不妨想想:我是要找「最常见的」,还是找「最中间的」?选对了工具,事半功倍。


公众号:蓝海资料掘金营,微信deep3321