原文链接: https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self
英文原文
You are given an integer array nums
and you have to return a new counts
array. The counts
array has the property where counts[i]
is the number of smaller elements to the right of nums[i]
.
Example 1:
Input: nums = [5,2,6,1] Output: [2,1,1,0] Explanation: To the right of 5 there are 2 smaller elements (2 and 1). To the right of 2 there is only 1 smaller element (1). To the right of 6 there is 1 smaller element (1). To the right of 1 there is 0 smaller element.
Example 2:
Input: nums = [-1] Output: [0]
Example 3:
Input: nums = [-1,-1] Output: [0,0]
Constraints:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
中文题目
给你一个整数数组 nums
,按要求返回一个新数组 counts
。数组 counts
有该性质: counts[i]
的值是 nums[i]
右侧小于 nums[i]
的元素的数量。
示例 1:
输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
示例 2:
输入:nums = [-1] 输出:[0]
示例 3:
输入:nums = [-1,-1] 输出:[0,0]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
通过代码
高赞题解
说明:
- 建议倍速观看视频。由于时间精力有限,没有做剪辑和修饰,感谢大家的理解;
- 「归并排序」的代码是按照《算法 4》这本书第 2.2 节的写法,与写几个
while
循环是等价的,归并回去的时候都需要考虑数组下标越界的情况; - 树状数组的知识讲解和写法可以参考:树状数组(Java 代码、Python 代码)。
归并排序计算逆序数 + 索引数组
使用「归并排序」求解这道问题,需要有求解「逆序对」的经验。可以先做一下「力扣」剑指 Offer 51. 数组中的逆序对;
- 求解「逆序对」的思想:当其中一个数字放进最终归并以后的有序数组中的时候,这个数字与之前看过的数字个数(或者是未看过的数字个数)可以直接统计出来,而不必一个一个数。这样排序完成以后,原数组的逆序数也就计算出来了;
- 具体来说,本题让我们求「在一个数组的某个元素的右边,比自己小的元素的个数」,因此,需要在「前有序数组」的元素归并的时候,数一数「后有序数组」已经归并回去的元素的个数,因为这些已经出列的元素都比当前出列的元素要(严格)小;
- 但是在「归并」的过程中,元素的位置会发生变化,因此下一步需要思考如何定位元素;根据「索引堆」的学习经验,一个元素在算法的执行过程中位置发生变化,我们还想定位它,可以使用「索引数组」,技巧在于:「原始数组」不变,用于比较两个元素的大小,真正位置变化的是「索引数组」的位置;
- 「索引数组」技巧建立了一个一一对应的关系,记录了当前操作的数对应的「原始数组」的下标,「索引数组」技巧想法的来源是「索引堆」(《算法(第 4 版)》第 2.4 节 练习);
- 「归并排序」还需要一个用于归并的辅助数组,这个时候拷贝的就是索引数组的值了。
参考代码:
[]import java.util.ArrayList; import java.util.List; public class Solution { public List<Integer> countSmaller(int[] nums) { List<Integer> result = new ArrayList<>(); int len = nums.length; if (len == 0) { return result; } int[] temp = new int[len]; int[] res = new int[len]; // 索引数组,作用:归并回去的时候,方便知道是哪个下标的元素 int[] indexes = new int[len]; for (int i = 0; i < len; i++) { indexes[i] = i; } mergeAndCountSmaller(nums, 0, len - 1, indexes, temp, res); // 把 int[] 转换成为 List<Integer>,没有业务逻辑 for (int i = 0; i < len; i++) { result.add(res[i]); } return result; } /** * 针对数组 nums 指定的区间 [left, right] 进行归并排序,在排序的过程中完成统计任务 * * @param nums * @param left * @param right */ private void mergeAndCountSmaller(int[] nums, int left, int right, int[] indexes, int[] temp, int[] res) { if (left == right) { return; } int mid = left + (right - left) / 2; mergeAndCountSmaller(nums, left, mid, indexes, temp, res); mergeAndCountSmaller(nums, mid + 1, right, indexes, temp, res); // 归并排序的优化,如果索引数组有序,则不存在逆序关系,没有必要合并 if (nums[indexes[mid]] <= nums[indexes[mid + 1]]) { return; } mergeOfTwoSortedArrAndCountSmaller(nums, left, mid, right, indexes, temp, res); } /** * [left, mid] 是排好序的,[mid + 1, right] 是排好序的 * * @param nums * @param left * @param mid * @param right * @param indexes * @param temp * @param res */ private void mergeOfTwoSortedArrAndCountSmaller(int[] nums, int left, int mid, int right, int[] indexes, int[] temp, int[] res) { for (int i = left; i <= right; i++) { temp[i] = indexes[i]; } int i = left; int j = mid + 1; for (int k = left; k <= right; k++) { if (i > mid) { indexes[k] = temp[j]; j++; } else if (j > right) { indexes[k] = temp[i]; i++; res[indexes[k]] += (right - mid); } else if (nums[temp[i]] <= nums[temp[j]]) { // 注意:这里是 <= ,保证稳定性 indexes[k] = temp[i]; i++; res[indexes[k]] += (j - mid - 1); } else { indexes[k] = temp[j]; j++; } } } public static void main(String[] args) { int[] nums = new int[]{5, 2, 6, 1}; Solution solution = new Solution(); List<Integer> countSmaller = solution.countSmaller(nums); System.out.println(countSmaller); } }
[]from typing import List class Solution: def countSmaller(self, nums: List[int]) -> List[int]: size = len(nums) if size == 0: return [] if size == 1: return [0] temp = [None for _ in range(size)] res = [0 for _ in range(size)] # 索引数组,作用:归并回去的时候,方便知道是哪个下标的元素 indexes = [i for i in range(size)] self.__merge_and_count_smaller(nums, 0, size - 1, temp, indexes, res) return res def __merge_and_count_smaller(self, nums, left, right, temp, indexes, res): if left == right: return mid = left + (right - left) // 2 self.__merge_and_count_smaller(nums, left, mid, temp, indexes, res) self.__merge_and_count_smaller(nums, mid + 1, right, temp, indexes, res) if nums[indexes[mid]] <= nums[indexes[mid + 1]]: return self.__sort_and_count_smaller(nums, left, mid, right, temp, indexes, res) def __sort_and_count_smaller(self, nums, left, mid, right, temp, indexes, res): # [left,mid] 前有序数组 # [mid+1,right] 后有序数组 # 先拷贝,再合并 for i in range(left, right + 1): temp[i] = indexes[i] i = left j = mid + 1 for k in range(left, right + 1): if i > mid: indexes[k] = temp[j] j += 1 elif j > right: indexes[k] = temp[i] i += 1 res[indexes[k]] += (right - mid) elif nums[temp[i]] <= nums[temp[j]]: indexes[k] = temp[i] i += 1 res[indexes[k]] += (j - mid - 1) else: indexes[k] = temp[j] j += 1 if __name__ == '__main__': nums = [5, 2, 6, 1] solution = Solution() result = solution.countSmaller(nums) print(result)
复杂度分析:
- 时间复杂度:$O(N \log N)$,数组的元素个数是 $N$,递归执行分治法,时间复杂度是对数级别的,因此时间复杂度是 $O(N \log N)$。
- 空间复杂度:$O(N)$,需要 $3$ 个数组,一个索引数组,一个临时数组用于索引数组的归并,还有一个结果数组,它们的长度都是 $N$,故空间复杂度是 $O(N)$。
补充:视频中的图片。
统计信息
通过次数 | 提交次数 | AC比率 |
---|---|---|
52323 | 125346 | 41.7% |
提交历史
提交时间 | 提交结果 | 执行时间 | 内存消耗 | 语言 |
---|
相似题目
题目 | 难度 |
---|---|
区间和的个数 | 困难 |
根据身高重建队列 | 中等 |
翻转对 | 困难 |