题目描述

✅ 1093. 大样本统计

image-20260929074757235

image-20260929074757372

image-20260929074757476

题意分析

样本中的整数都在零到二百五十五之间,输入 count[i] 表示数值 i 出现了多少次。按照最小值、最大值、平均数、中位数、众数的顺序,返回五个浮点统计量。

输入是频次表,数组下标才是样本值,数组元素是出现次数。样本总数至少为一,众数保证唯一。中位数按所有样本排序后的位置确定,平均数按全部出现加权计算,不能把不同值简单平均,也不应把巨大样本重新展开。

解法:两趟频次统计

核心思路

[!blue]

按下标从小到大扫描频次表。第一个频次非零的下标就是最小值,最后一个频次非零的下标就是最大值;频次最大的下标是众数,注意返回的是对应数值,而不是这个最大频次。

每个数值 i 为总数量贡献 count[i],为总和贡献 i * count[i]。分别累计为 total 和 sum,平均数就是 sum / total。加权总和可能超过 32 位整数,乘法前就要将操作数提升为 64 位,不能等乘法溢出后再转换。做除法时也要先转换为浮点数,保留小数部分。

中位数不用真的排序展开。采用一基名次,统一取 (total + 1) / 2 和 (total + 2) / 2,这里是整数除法。总数为奇数时,两者都是同一个中间名次;总数为偶数时,它们就是相邻的两个中间名次,最后求两个对应值的平均数。

第二次按数值递增累加频次。若某个值之前累计了 before 个样本,它的频次为 c,那么排序后第 before + 1 到第 before + c 个位置都是这个值。累计数量第一次达到某个目标名次时,当前下标就是该名次的值,即使一次越过多个位置也不需要逐个展开。

左中位值只在第一次达到左名次时保存,不能继续覆盖;右名次达到时即可结束扫描。两个目标名次也可能落在同一频次段中,此时两个中位值相同,同样用平均公式处理。零是合法样本值,因此未赋值状态使用值域外的 -1。

解题步骤

  1. 第一遍扫描非零频次,确定最小值、最大值、众数,并累计总数和加权总和。
  2. 用浮点除法计算平均数,再确定两个中间名次。
  3. 第二遍从小到大累计频次,分别记录第一次覆盖这两个名次的样本值。
  4. 求两个中间值的浮点平均数,按题目指定顺序返回五项。

代码实现

class Solution {
    public double[] sampleStats(int[] count) {
        int min = -1;
        int max = -1;
        int mode = 0;
        long total = 0;
        long sum = 0;
        int modeCnt = 0;

        for (int i = 0; i < count.length; i++) {
            if (count[i] > 0) {
                if (min == -1) {
                    min = i;
                }

                max = i;
                total += count[i];
                // 先提升到 64 位再乘频次,防止整数乘法溢出。
                sum += (long) i * count[i];

                if (count[i] > modeCnt) {
                    modeCnt = count[i];
                    mode = i;
                }
            }
        }

        double mean = (double) sum / total;

        // 两个名次统一处理奇偶总数,名次从 1 开始。
        long mid1 = (total + 1) / 2;
        long mid2 = (total + 2) / 2;
        int m1 = -1;
        int m2 = -1;
        long acc = 0;

        for (int i = 0; i < count.length; i++) {
            acc += count[i];

            // 只记录第一次跨过左中位数名次的值。
            if (acc >= mid1 && m1 == -1) {
                m1 = i;
            }

            if (acc >= mid2) {
                m2 = i;
                break;
            }
        }

        double median = (m1 + m2) / 2.0;

        return new double[] {
            min,
            max,
            mean,
            median,
            mode
        };
    }
}
func sampleStats(count []int) []float64 {
    minVal := -1
    maxVal := -1
    mode := 0
    var total int64
    var sum int64
    modeCnt := 0

    for i, c := range count {
        if c > 0 {
            if minVal == -1 {
                minVal = i
            }
            maxVal = i
            total += int64(c)
            // 先提升到 64 位再乘频次,防止整数乘法溢出。
            sum += int64(i) * int64(c)
            if c > modeCnt {
                modeCnt = c
                mode = i
            }
        }
    }

    mean := float64(sum) / float64(total)

    // 两个名次统一处理奇偶总数,名次从 1 开始。
    mid1 := (total + 1) / 2
    mid2 := (total + 2) / 2
    var acc int64
    m1, m2 := -1, -1
    for i, c := range count {
        acc += int64(c)
        // 只记录第一次跨过左中位数名次的值。
        if acc >= mid1 && m1 == -1 {
            m1 = i
        }
        if acc >= mid2 {
            m2 = i
            break
        }
    }
    median := float64(m1+m2) / 2.0

    return []float64{
        float64(minVal),
        float64(maxVal),
        mean,
        median,
        float64(mode),
    }
}

复杂度分析

  • 时间复杂度:$O(C)$,C 为频次表长度,两次扫描都只处理表项,不依赖展开后的样本总数;本题 C = 256,为固定规模。
  • 空间复杂度:$O(1)$,只保存固定数量的统计变量和五项返回结果。

关键点总结

[!green]

  • 频次表同时给出数值顺序和重复数量,可以直接计算所有统计量。
  • 加权求和使用数值乘次数,中间排名用累计次数定位。
  • 两个中间名次统一处理奇偶,极值与中位值的未赋值状态不能占用合法样本零。

易错点总结

[!yellow]

  • 将 count[i] 当作样本值,会混淆数据与它的次数。
  • 先做 32 位乘法再转成 64 位,总和加入之前的乘积就可能已经溢出。
  • 先做整数除法再转浮点,平均数的小数部分已经丢失;中位数也应使用浮点除法。
  • 每次累计频次超过左名次都覆盖左中位值,会把它错误移动到更大的数值。
  • 用是否恰好等于目标名次来寻找中位数,会漏掉一次频次累加跨过名次的情况;应判断大于等于。
  • 将众数返回为最高出现次数,或者按五项的错误顺序组装结果,都不符合输出约定。

相似题目

题目 难度 关联与区别
347. 前 K 个高频元素 中等 频次最大者是众数,本题还需结合频次和数值计算平均数、中位数等统计量。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2025/41912314
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!