题目地址:
https://leetcode.com/problems/count-of-smaller-numbers-after-self/
给定一个长 n n n数组 A A A,要求返回一个数组 B B B,其中 B [ i ] B[i] B[i]是 A [ i ] A[i] A[i]右边比 A [ i ] A[i] A[i]小的数的个数。
法1:归并排序。这题实际上是在求以每个位置为第一个数的逆序对的个数是多少。关于逆序对总数的求法,可以参考https://blog.csdn.net/qq_46105170/article/details/113794612。本质上是在做归并排序的过程中,累加逆序对数量,并且顺便把数组排好序。而这题稍微复杂一些,它需要把每个位置的逆序对数量都求出来。可以先开一个数组 e e e, e = [ 0 , 1 , 2 , . . . , n − 1 ] e=[0,1,2,...,n-1] e=[0,1,2,...,n−1],接着对 e e e数组按照 A [ e [ i ] ] A[e[i]] A[e[i]]来排序,即最后要使得 A [ e [ 0 ] ] ≤ A [ e [ 1 ] ] ≤ A [ e [ 2 ] ] ≤ . . . ≤ A [ e [ n − 1 ] ] A[e[0]]\le A[e[1]]\le A[e[2]]\le ...\le A[e[n-1]] A[e[0]]≤A[e[1]]≤A[e[2]]≤...≤A[e[n−1]],这样在归并排序的过程中,我们就能不但知道每个位置的数值,还能知道它的下标,这样就方便统计每个位置的逆序对的数量了。代码如下:
import java.util.ArrayList;
import java.util.List;
public class Solution {
public List<Integer> countSmaller(int[] nums) {
int n = nums.length;
// cnt[i]是以nums[i]为第一个数的逆序对的数量,idxs是待排序的数组
int[] cnt = new int[n], idxs = new int[n];
for (int i = 0; i < n; i++) {
idxs[i] = i;
}
mergeSort(0, n - 1, cnt, idxs, nums, new int[n]);
List<Integer> res = new ArrayList<>();
for (int x : cnt) {
res.add(x);
}
return res;
}
// 对idxs[l:r]按照nums[l:r]的大小关系来从小到大排序,并且累加这一段区间的逆序对数量。
// 由于归并排序需要一个额外数组,tmp便是该数组
private void mergeSort(int l, int r, int[] cnt, int[] idxs, int[] nums, int[] tmp) {
if (l >= r) {
return;
}
int mid = l + (r - l >> 1);
mergeSort(l, mid, cnt, idxs, nums, tmp);
mergeSort(mid + 1, r, cnt, idxs, nums, tmp);
merge(l, r, mid, idxs, cnt, nums, tmp);
}
private void merge(int l, int r, int mid, int[] idxs, int[] cnt, int[] nums, int[] tmp) {
int i = l, j = mid + 1, pos = l;
while (i <= mid && j <= r) {
// 如果nums[idxs[i]] <= nums[idxs[j]],那么从mid + 1到j - 1都能与i产生逆序对,个数是j - mid - 1
if (nums[idxs[i]] <= nums[idxs[j]]) {
cnt[idxs[i]] += j - mid - 1;
tmp[pos++] = idxs[i++];
} else {
tmp[pos++] = idxs[j++];
}
}
while (i <= mid) {
cnt[idxs[i]] += j - mid - 1;
tmp[pos++] = idxs[i++];
}
while (j <= r) {
tmp[pos++] = idxs[j++];
}
// 归并排序里要把数组从tmp里重新填回待排序数组idxs中
for (int k = l; k <= r; k++) {
idxs[k] = tmp[k];
}
}
}
时间复杂度 O ( n log n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)。
法2:树状数组。可以先参考这一题https://blog.csdn.net/qq_46105170/article/details/108429714,这道题是求 A [ i ] A[i] A[i]左边比 A [ i ] A[i] A[i]小的数的个数。本题中我们只需要逆序遍历数组 A A A即可,最后再把答案翻转一下。但这题的数字可能有负数,我们需要将每个数都减去数组最小值,使之都变成非负数,这样方便树状数组操作。代码如下:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Solution {
class FenwickTree {
int[] tree;
// 对于长度为n的数组,构造其对应的树状数组
public FenwickTree(int n) {
tree = new int[n + 1];
}
// 在原数组的第i个(i从1开始取)数上加上x,对应的树状数组的操作
public void add(int i, int x) {
while (i < tree.length) {
tree[i] += x;
i += lowbit(i);
}
}
// 求原数组的前i个(i也是从1开始取)数的和
public int sum(int i) {
int res = 0;
while (i > 0) {
res += tree[i];
i -= lowbit(i);
}
return res;
}
private int lowbit(int x) {
return x & -x;
}
}
public List<Integer> countSmaller(int[] nums) {
List<Integer> res = new ArrayList<>();
if (nums.length == 0) {
return res;
}
int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
// 先求出nums的最小值
for (int num : nums) {
min = Math.min(min, num);
}
// 向右平移成非负数
for (int i = 0; i < nums.length; i++) {
nums[i] -= min;
max = Math.max(max, nums[i]);
}
// 想象开一个长度为max + 1的数组C
FenwickTree tree = new FenwickTree(max + 1);
for (int i = nums.length - 1; i >= 0; i--) {
// 求C的前nums[i]个数的和
res.add(tree.sum(nums[i]));
// 将数组C的第nums[i] + 1个数增加1
tree.add(nums[i] + 1, 1);
}
// 翻转一下res再返回
Collections.reverse(res);
return res;
}
}
时间复杂度 O ( n log ( M − m ) ) O(n\log (M-m)) O(nlog(M−m)),空间 O ( M − m ) O(M-m) O(M−m), m m m和 M M M分别是数组最小和最大值。