在问 · 算法题解

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

困难二分查找从右往左二分插入有序数组力扣原题 ↗

题目描述

输入一个整数数组 nums。

返回一个与它等长的数组 counts,其中 counts[i] 等于下标在 i 之后、并且严格小于 nums[i] 的元素个数。

示例 1
输入:nums = [5,2,6,1]
输出:[2,1,1,0]

解释:5 右边比它小的有 2 和 1;2 右边有 1;6 右边有 1;1 右边没有元素。

示例 2
输入:nums = [-1]
输出:[0]

解释:只有一个元素,右边为空。

示例 3
输入:nums = [-1,-1]
输出:[0,0]

解释:相等的数不算更小。

数据范围

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

解题思路:从右往左二分插入有序数组

从右往左扫描,把扫过的数放进一个始终有序的列表 seen。处理 nums[i] 时,seen 恰好是它右边的所有数;在 seen 上用 bisect_left 二分出 nums[i] 的插入位置 pos,pos 左边的数都严格小于 nums[i],所以答案就是 pos。再把 nums[i] 插到 pos,保持有序。

为什么正确

处理到下标 i 时,seen 是 nums[i+1:] 的升序排列。

复杂度分析

二分 O(log n),列表插入 O(n),总时间最坏 O(n^2)(实际很快;要严格 O(n log n) 可换树状数组或归并排序);空间 O(n)。

易错点

必须用 bisect_left(第一个 >= x 的位置)。用 bisect_right 会把右边等于 nums[i] 的数也算成“更小”。

参考代码

Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。

class Solution:
    def countSmaller(self, nums: List[int]) -> List[int]:
        seen = []
        res = [0] * len(nums)
        for i in range(len(nums) - 1, -1, -1):
            pos = bisect_left(seen, nums[i])
            res[i] = pos
            seen.insert(pos, nums[i])
        return res

载入编辑器,自己跑一遍 →

其他解法

主解法直观,但列表插入是 O(n),最坏 O(n²);归并排序在合并时顺带统计“从右边越过来的更小元素”;树状数组把问题变成值域上的前缀计数。后两者都是严格的 O(n log)。

解法 2:归并排序统计 时间 O(n log n),空间 O(n)

对下标数组按值做归并排序。合并左右两个有序段时,若左段元素 nums[l[i]] <= 右段当前元素,就放入左段元素,并把右段已弹出的个数 j 累加到 res[l[i]]:这 j 个元素都在它右边,且严格更小。左段剩余元素同样累加 j。

最坏情况也是 O(n log n),且不依赖值域,是“逆序对”类问题的通用模板。要排序下标而不是值,还要注意相等时先放左边,细节较多。

class Solution:
    def countSmaller(self, nums: List[int]) -> List[int]:
        n = len(nums)
        res = [0] * n

        def sort(idx):
            if len(idx) <= 1:
                return idx
            mid = len(idx) // 2
            left, right = sort(idx[:mid]), sort(idx[mid:])
            merged = []
            j = 0
            for i in left:
                while j < len(right) and nums[right[j]] < nums[i]:
                    merged.append(right[j])
                    j += 1
                res[i] += j
                merged.append(i)
            merged.extend(right[j:])
            return merged

        sort(list(range(n)))
        return res

解法 3:树状数组(值域计数) 时间 O(n log V),空间 O(V)

从右往左遍历,用树状数组维护“右侧已出现的每个值的个数”。先把值域 [-10^4, 10^4] 平移到 [1, 20001];处理 nums[i] 时,前缀和 query(v-1) 就是右侧严格更小的个数,然后 update(v, 1)。

代码短,单次操作 O(log V)。当值域很大时要先离散化(排序去重后映射到排名)。

class Solution:
    def countSmaller(self, nums: List[int]) -> List[int]:
        OFFSET, SIZE = 10001, 20002
        tree = [0] * (SIZE + 1)

        def update(i):
            while i <= SIZE:
                tree[i] += 1
                i += i & -i

        def query(i):
            s = 0
            while i > 0:
                s += tree[i]
                i -= i & -i
            return s

        res = [0] * len(nums)
        for i in range(len(nums) - 1, -1, -1):
            v = nums[i] + OFFSET
            res[i] = query(v - 1)
            update(v)
        return res

二分查找 专题的其他题目

查看 二分查找 全部题目 →