目录

题目描述

315. 计算右侧小于当前元素的个数

题意分析

给一个整数数组 nums,对每个下标 i 求出 counts[i]:满足 j > inums[j] < nums[i] 的下标 j 的个数。返回和原数组等长的答案数组。

要什么:每个位置都要一个答案,不是一个总和。这一点和「逆序对总数」那类题不同,后者只要一个数字,本题要求把贡献落到每个下标上,所以统计过程必须能定位到具体位置。

严格小于是硬性的:nums[j] == nums[i] 不计入。相等值的处理是本题最常见的错误来源。

约束信号:1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^410^5 直接否掉了 $O(n^2)$ 的双重循环(约 $10^{10}$ 次比较,必然超时),要求达到 $O(n \log n)$。值域是 [-10^4, 10^4],含负数、且左右都有界——负数意味着不能拿值直接当数组下标,有界则暗示可以做值域压缩或直接开桶。

边界要想到:数组含大量重复值;全是负数;n = 1 时答案是 [0];数组本身已经升序时答案全 0,降序时答案是 n-1, n-2, …, 0

解法:归并排序统计逆序关系

核心思路

暴力枚举每个位置右侧的元素需要 $O(n^2)$。这类“左边元素与右边元素形成多少逆序关系”的问题,可以在归并排序合并两个有序段时一次统计。

排序过程中不直接搬运数值,而是搬运原下标 indexes,这样计数能写回对应答案。合并左右有序段时维护 rightMoved,表示已经放入临时数组的右段元素个数:

  • 若右段值更小,先放右段元素,并令 rightMoved++
  • 若放左段元素,此前移走的右段元素都位于它的原始右侧,且都严格小于它,因此给该左段元素的答案加上 rightMoved

相等时必须先放左段元素,因为题目只统计“严格小于”。归并完成后,当前区间按值有序,计数也已完整覆盖左半段与右半段之间的所有逆序关系;递归则负责各自区间内部的关系。

解题步骤

  • 初始化 indexes[i] = i、临时数组 temp 和计数数组 counts
  • 递归排序半开区间 [left, right);长度不超过 1 时返回。
  • 合并时比较 nums[indexes[i]]nums[indexes[j]]。右侧更小时先放右侧并增加 rightMoved;否则先放左侧,并把 rightMoved 加到该原下标的计数中。
  • 将合并后的下标序列复制回 indexes,保证上一层看到的是有序区间。
  • counts 转为题目要求的结果类型。

[5, 2, 6, 1] 为例:合并 [5][2] 时,2 先移走,5 的计数加 1;合并 [6][1] 时,6 的计数加 1。最终合并有序段 [2, 5][1, 6],1 先移走,2 和 5 的计数各再加 1,得到 [2, 1, 1, 0]

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {
    public List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        int[] indexes = new int[n];
        int[] temp = new int[n];
        int[] counts = new int[n];
        for (int i = 0; i < n; i++) {
            indexes[i] = i;
        }
        mergeSort(nums, indexes, temp, counts, 0, n);

        List<Integer> answer = new ArrayList<>(n);
        for (int count : counts) {
            answer.add(count);
        }
        return answer;
    }

    private void mergeSort(int[] nums, int[] indexes, int[] temp,
                           int[] counts, int left, int right) {
        if (right - left <= 1) {
            return;
        }
        int mid = left + (right - left) / 2;
        mergeSort(nums, indexes, temp, counts, left, mid);
        mergeSort(nums, indexes, temp, counts, mid, right);

        int i = left;
        int j = mid;
        int rightMoved = 0;
        for (int k = left; k < right; k++) {
            if (j == right ||
                    (i < mid && nums[indexes[i]] <= nums[indexes[j]])) {
                counts[indexes[i]] += rightMoved;
                temp[k] = indexes[i++];
            } else {
                temp[k] = indexes[j++];
                rightMoved++;
            }
        }
        for (int k = left; k < right; k++) {
            indexes[k] = temp[k];
        }
    }
}
func countSmaller(nums []int) []int {
    n := len(nums)
    indexes := make([]int, n)
    temp := make([]int, n)
    counts := make([]int, n)
    for i := range indexes {
        indexes[i] = i
    }
    mergeCount(nums, indexes, temp, counts, 0, n)
    return counts
}

func mergeCount(nums, indexes, temp, counts []int, left, right int) {
    if right-left <= 1 {
        return
    }
    mid := left + (right-left)/2
    mergeCount(nums, indexes, temp, counts, left, mid)
    mergeCount(nums, indexes, temp, counts, mid, right)

    i, j, rightMoved := left, mid, 0
    for k := left; k < right; k++ {
        if j == right || (i < mid && nums[indexes[i]] <= nums[indexes[j]]) {
            counts[indexes[i]] += rightMoved
            temp[k] = indexes[i]
            i++
        } else {
            temp[k] = indexes[j]
            j++
            rightMoved++
        }
    }
    copy(indexes[left:right], temp[left:right])
}

复杂度分析

  • 时间复杂度:$O(n \log n)$。归并排序有 $O(\log n)$ 层,每层合并全部 n 个下标。
  • 空间复杂度:$O(n)$。下标数组、临时数组和答案数组占线性空间,递归栈为 $O(\log n)$。

关键点总结

  • 必须排序原下标而不是只排序数值,否则无法把计数写回原位置。
  • rightMoved 只统计已经越过当前左元素的右段元素;它们原本在右侧,且值更小,正好是答案贡献。
  • 相等时先取左段,才能排除相等元素,落实“严格小于”。
  • 这与普通逆序对归并的区别在于:普通题只累加总数,本题要把贡献记到每个左元素的原下标。
  • 树状数组加坐标压缩也是 $O(n \log n)$ 解法;归并法不依赖值域,更适合从逆序对模型直接推导。

易错点总结

  • 相等时先取右段:[1, 1] 会把右边相等的 1 错算为更小,得到 [1, 0]
  • 只累加跨段总数:能求出整个数组的逆序对数,却无法回答每个下标各有多少个。
  • 排序数值而不携带原下标:排序后失去答案与原位置的对应关系。
  • 合并后忘记复制回 indexes:上一层拿到的子区间并未有序,后续计数失效。
  • 给右段元素增加计数:题目统计的是当前元素右侧更小的数量,贡献应记在被越过的左段元素上。

相似题目

题目 难度 考察点
307. 区域和检索 - 数组可修改 中等 树状数组单点改 + 区间查的裸题
327. 区间和的个数 困难 前缀和上的区间计数
493. 翻转对 困难 带系数的跨段比较
剑指 Offer 51. 数组中的逆序对 困难 只求逆序对总数,不落到每个下标
补充题 8. 计算数组的小和 中等 逆序对统计的加权求和变形
面试题-最小交换次数(任意交换与相邻交换) 困难 逆序对数即相邻交换的最少次数