目录

题目描述

1365. 有多少小于当前数字的数字

题意分析

给定数组 nums,对每个下标 i 统计数组中有多少个 j($j \ne i$)满足 nums[j] < nums[i],把所有答案按原下标顺序组成数组返回。

题面里有两个词必须抠死。一是「小于」,是严格小于,等于当前数字的元素不能计入;二是「$j \ne i$」,看似要求排除自己,但因为比较是严格小于、而 nums[i] < nums[i] 恒为假,自己本来就不会被数进去,所以这个条件其实不需要任何额外处理——这一点值得在面试里主动说出来,说明你读懂了题而不是照抄条件。

约束是本题唯一的算法信号:$1 \le n \le 500$,$0 \le nums[i] \le 100$。数组长度小到 $O(n^2)$ 都能过,但值域被死死限制在 0 到 100 这 101 个整数上,这是一个非常刺眼的提示:值域比数组长度还小,说明可以用值域直接开桶,而不是去排序或用哈希表。凡是「值域是小常数」的题,第一反应都应该是计数数组。

还要注意返回的是按原下标排列的答案数组,不是排序后的数组。所以任何打乱原顺序的做法(比如原地排序 nums)都必须想清楚怎么把答案映射回原位置。

边界有三处。数组只有一个元素时,答案是 [0]。所有元素相同时,因为是严格小于,答案全是 0。值 0 是合法输入,且没有任何数小于它,所以 0 对应的答案恒为 0——这一点决定了前缀数组的初始项必须是 0。

解法:计数数组 + 前缀和

核心思路

暴力做法是两层循环:对每个 i 扫一遍整个数组,数出比它小的元素个数。$n \le 500$ 时这个 $O(n^2)$ 的做法完全能过,写法也没有陷阱。但它的浪费很明显:如果 nums[i] == nums[k],那么它们的答案必然相同,暴力却把同一次统计重复做了两遍。换句话说,答案只是「值」的函数,与下标无关,而值最多只有 101 种。

顺着这个观察走:真正要求的是一个函数 $g(v)$ = 数组中严格小于 $v$ 的元素个数,然后把每个 nums[i] 拿去查表就行。而 $g$ 显然可以由频次累加得到——先统计每个值出现了几次,再对频次求前缀和。

于是定义两个数组。freq[v] 表示值 vnums 中出现的次数,$v \in [0, 100]$。prefix[v] 表示 nums严格小于 v 的元素个数,也就是 $\sum_{u < v} freq[u]$。

这两个定义之间的递推关系是 prefix[v] = prefix[v - 1] + freq[v - 1],边界是 prefix[0] = 0。这条递推最容易写错的地方在于右边加的是 freq[v - 1] 而不是 freq[v]prefix[v] 要的是「小于 v」,所以它等于「小于 v-1 的个数」加上「恰好等于 v-1 的个数」。如果误写成 prefix[v] = prefix[v-1] + freq[v],得到的就是「小于等于 v」的语义,相等的元素会被错误地计入。这个「严格小于 / 小于等于」的错位就是本题最核心的考点。

prefix[0] = 0 这个初值同时承担了两个含义:没有任何整数小于 0(因为值域下界就是 0),以及前缀和递推需要一个空前缀作为起点。它天然由数组的零值初始化提供,不需要额外写。

最后一步,按原下标顺序遍历 numsanswer[i] = prefix[nums[i]]。因为 prefix 是按值索引的,查表是 $O(1)$,且原数组的顺序完全没有被破坏,返回的答案自动就是题目要的下标顺序。

作为对照,另一条常见思路是「排序后回填」:把 nums 拷贝一份排序,然后对每个值找它在有序数组中第一次出现的下标,那个下标就是小于它的元素个数。这个做法是 $O(n \log n)$,且必须用「第一次出现」而不是任意出现位置,否则重复值会算错。值域这么小的时候,计数 + 前缀和更简单也更快,是这题的标准答案。

解题步骤

  • 开一个长度 101 的频次数组:下标范围要覆盖 [0, 100] 全闭区间,所以长度是 101 而不是 100。少开一格会在遇到值 100 时直接数组越界。
  • 一次遍历统计频次freq[num]++。这一步不关心下标,只关心值,是「答案只依赖值」这个观察的直接体现。
  • 开一个长度 101 的前缀数组并递推for (int i = 1; i <= 100; i++) prefix[i] = prefix[i - 1] + freq[i - 1];。循环必须从 1 开始,因为 prefix[0] 是不需要计算的边界(恒为 0),从 0 开始会访问 prefix[-1]。上界写 <= 100 是因为值 100 也可能出现在 nums 里,它需要一个有效的查询结果。
  • 注意这一步和「值是否真的出现过」无关:即使某个值一次都没出现,prefix 在那个位置也会被正确填成「小于它的元素个数」。前缀数组是对整个值域填满的,不是只对出现过的值填。
  • 按原顺序查表生成答案answer[i] = prefix[nums[i]]。这里绝不能对 nums 做任何重排,否则下标对应关系就断了。

nums = [8, 1, 2, 2, 3] 走一遍(正确答案 [4, 0, 1, 1, 3])。

第一遍统计频次:freq[1] = 1freq[2] = 2freq[3] = 1freq[8] = 1,其余全为 0。

第二遍递推前缀。prefix[0] = 0prefix[1] = prefix[0] + freq[0] = 0 + 0 = 0(没有元素小于 1)。prefix[2] = prefix[1] + freq[1] = 0 + 1 = 1(只有那个 1)。prefix[3] = prefix[2] + freq[2] = 1 + 2 = 3(1 和两个 2)。prefix[4] = prefix[3] + freq[3] = 3 + 1 = 4prefix[5]prefix[8] 因为 freq[4]freq[7] 全是 0,一路保持 4。

第三遍查表:nums[0] = 8 → prefix[8] = 4nums[1] = 1 → prefix[1] = 0nums[2] = 2 → prefix[2] = 1nums[3] = 2 → prefix[2] = 1nums[4] = 3 → prefix[3] = 3。拼起来是 [4, 0, 1, 1, 3],与答案一致。

特别看下标 2 和 3 这两个都等于 2 的元素:它们查的是同一格 prefix[2],自然得到相同的答案 1,而且这个 1 里不包含另一个 2——因为 prefix[2] 的定义是严格小于 2。如果递推误写成 prefix[i] = prefix[i-1] + freq[i]prefix[2] 会变成 0 + 2 = 2,两个 2 各自把对方(以及自己)算了进去,输出变成 [4, 1, 2, 2, 4],五个位置里错了四个。

代码实现

class Solution {
    public int[] smallerNumbersThanCurrent(int[] nums) {
        // 值域是 [0, 100] 闭区间,长度必须是 101。
        int[] freq = new int[101];
        for (int num : nums) {
            freq[num]++;
        }

        // prefix[v] 表示严格小于 v 的元素个数,prefix[0] = 0 由零值初始化天然成立。
        int[] prefix = new int[101];
        for (int i = 1; i <= 100; i++) {
            // 加的是 freq[i - 1] 而不是 freq[i],否则语义会滑成「小于等于 i」。
            prefix[i] = prefix[i - 1] + freq[i - 1];
        }

        // 按原下标顺序查表,不能重排 nums,否则答案与位置的对应关系就断了。
        int[] answer = new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            answer[i] = prefix[nums[i]];
        }

        return answer;
    }
}
func smallerNumbersThanCurrent(nums []int) []int {
    // 值域是 [0, 100] 闭区间,长度必须是 101。
    freq := make([]int, 101)
    for _, num := range nums {
        freq[num]++
    }

    // prefix[v] 表示严格小于 v 的元素个数,prefix[0] = 0 由零值初始化天然成立。
    prefix := make([]int, 101)
    for i := 1; i <= 100; i++ {
        // 加的是 freq[i-1] 而不是 freq[i],否则语义会滑成「小于等于 i」。
        prefix[i] = prefix[i-1] + freq[i-1]
    }

    // 按原下标顺序查表,不能重排 nums,否则答案与位置的对应关系就断了。
    answer := make([]int, len(nums))
    for i, num := range nums {
        answer[i] = prefix[num]
    }

    return answer
}

复杂度分析

  • 时间复杂度:$O(n + C)$,其中 $n$ 是数组长度、$C = 101$ 是值域大小。统计频次和查表各扫一遍数组共 $O(n)$,递推前缀和扫一遍值域 $O(C)$。相比排序法的 $O(n \log n)$,它是线性的,代价是必须知道值域上界。
  • 空间复杂度:$O(C)$,两个长度 101 的辅助数组,与输入规模无关;结果数组是题目要求的输出,通常不计入额外空间。如果想省一半,可以只用一个数组做原地前缀和(先统计频次再就地累加,但要注意此时数组语义已变),本题没有必要。

关键点总结

  • 看到「值域是个小常数」(这里是 0 到 100)就该联想到计数数组:用值当下标开桶,可以把很多 $O(n \log n)$ 或 $O(n^2)$ 的统计降到 $O(n + C)$。这是计数排序家族的共同入口。
  • 「答案只依赖值、不依赖下标」是本题从暴力走向优化的关键观察。凡是发现相同输入被重复计算,就应该考虑把结果按「输入的取值」缓存下来。
  • 前缀和的语义边界必须写死在纸上:这里 prefix[v]严格小于 v,所以递推里加的是 freq[v - 1]。「小于」和「小于等于」的一格之差是这类题最高频的错误来源,写代码前先把定义念一遍。
  • 值域数组要按闭区间长度开([0, 100] 对应 101 格),并且要为值域内所有取值填好前缀,而不是只填出现过的值——否则查询未出现的值时会拿到脏数据。
  • 面试里可以把三种做法排成一条线来答:$O(n^2)$ 暴力 → $O(n \log n)$ 排序找首次出现位置 → $O(n + C)$ 计数前缀和,并说明第三种成立的前提是值域有界。能主动指出「$j \ne i$ 这个条件因为是严格小于所以不需要额外处理」通常也是加分项。

易错点总结

  • 递推写成 prefix[i] = prefix[i - 1] + freq[i]:语义变成「小于等于 i」。nums = [8,1,2,2,3] 会输出 [4,1,2,2,4] 而不是 [4,0,1,1,3],每个值都把与自己相等的元素(含自己)算了进去。
  • 频次数组只开 100 格nums 中出现 100 时 freq[100]++ 直接数组越界,而这个值完全合法。
  • 前缀循环从 i = 0 开始prefix[0] = prefix[-1] + freq[-1] 直接越界;prefix[0] 本来就该是 0,不需要参与递推。
  • 前缀循环上界写成 i < 100prefix[100] 永远是 0,输入里任何等于 100 的元素答案都会是 0。例如 nums = [100, 1] 会输出 [0, 0],正确答案是 [1, 0]
  • 为了排序方便直接对 nums 原地排序再回填:原下标信息被破坏,nums = [8,1,2,2,3] 排完变成 [1,2,2,3,8],即使算对了也无法还原成题目要求的下标顺序。
  • 用排序法时取「值在有序数组中的任意出现位置」而非首次出现位置[1,2,2,3] 中第二个 2 的下标是 2,会得到答案 2,而正确答案是 1,重复值全部偏大。
  • 误以为要显式排除 j == i 而把答案减一:因为比较是严格小于,自己从来没被计入,减一会让所有非最小元素的答案偏小 1。
  • 把答案数组写成 new int[101] 而不是 new int[nums.length]:返回的数组长度与输入不符,判题直接失败。
  • 用哈希表代替计数数组但忘了未出现的值:查询 map.get(v) 时若 v 没出现过会拿到 null 触发空指针,用计数数组则天然是 0。

相似题目

题目 难度 考察点
1122. 数组的相对排序 简单 同样靠值域开桶,但输出是重排后的数组,且顺序由第二个数组指定
LCR 075. 数组的相对排序 简单 与 1122 同题,可直接套用同一份计数桶写法
1636. 按照频率将数组升序排序 简单 计数之后不是求前缀,而是把频次当作排序的主关键字
315. 计算右侧小于当前元素的个数 困难 只统计右侧且值域很大,静态前缀和失效,需要树状数组或归并排序在线维护
912. 排序数组 中等 值域有界时计数排序 $O(n + C)$ 优于比较排序,本题正是它的统计阶段
338. 比特位计数 简单 同样是「按值域填满一张表、用前一项推当前项」的递推填表套路
274. H 指数 中等 值被 n 截断后开桶,但求的是后缀和(统计不小于 h 的个数),方向相反