题目描述:
给你一个整数数组 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 个更小的元素
思路:一开始的思路是从后往前遍历,然后当前元素i的后边元素保持降序sort(i+1, n)。但是最后几个测试用例会超时。
代码1:
class Solution {
public List<Integer> countSmaller(int[] nums) {
int n = nums.length;
int[] counts = new int[n];
int min = nums[n-1];
counts[n-1] = 0;
for (int i = n-2; i >= 0; i--) {
int j = i+1;
int cur = nums[i];
if (n > 49999 && cur < min) { //几个特殊用例可以打补丁防止超时但仍然不能ac
min = cur;
counts[i] = 0;
continue;
}
while (j < n && cur <= nums[j]) {
nums[j-1] = nums[j];
j++;
}
nums[j-1] = cur;
counts[i] = n-j;
//Arrays.sort(nums, cmp, i, n);
}
List<Integer> res = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
res.add(counts[i]);
}
return res;
}
}
思路2:
上边的代码可以过掉64/66个用例(虽然偷懒了)。但其他的解法确实想不到了(后来看了题解可以优化成二分插入,时间复杂度由O(n^2)降到O(nlogn)列在代码2)。于是评论区看题解发现树状数组,归并排序,二分查找等都是没用过比较陌生的知识点,于是将笔记汇总于此。
代码2(有序数组+二分查找):
class Solution {
public List<Integer> countSmaller(int[] nums) {
int n = nums.length;
List<Integer> list = new ArrayList<Integer>(); //用于存储右侧部分值,执行二分查找+插入
int[] counts = new int[n]; //记录结果
counts[n-1] = 0;
list.add(nums[n-1]);
for (int i = n-2; i >= 0; i--) {
int cur = nums[i];
int left = 0;
int right = list.size()-1;
//二分查找
while (left <= right) {
int mid = (left + right) / 2;
if (cur > list.get(mid)) left = mid + 1;
else right = mid - 1;
}
list.add(left, cur);
counts[i] = left;
}
List<Integer> res = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
res.add(counts[i]);
}
return res;
}
}
树状数组就是利用到了十进制与二进制之间的关系,是一个非常巧妙的用于求解前缀和的数据结构。树状数组的原理可以在b站上搜到,很多up用动画讲解的非常到位。这里给出一张图简单解释一下。
对于一个有序数组,当我们求前缀和的时候,不必要依次累加,而是用若干区间累加即可。例如 sum(1,5)=sum(1,4)+sum(4,5)。那这个向下递减的过程就是靠二进制实现的,(5)2=101,用它减去最右侧的1及其后的0之后变成(4)2=100。递减过程中需保证其大于0(因为数组是从1开始的)。这个求最右侧的1及其后的0是通过lowbit函数实现的,具体原理涉及计租中的补码和反码,这里不详细展开了(我也不太明白)。通过树状数组,我们可以在O(logn)的时间内求出任意位置的前缀和。
第二个问题就是树状数组的维护,在普通数组中,更新一个数值只需更改nums[i]的数值即可。但是在树状数组中,一个位置可能被多个区间包含,比如上图中的nums[2]对应四个区间tree[2],tree[4],tree[8],tree[16]。因此更新nums[2]的数值需要更新四个树状数组的数值。这其实就是反向加lowbit即可。(因为tree[2]就是通过减lowbit得到的嘛)
代码3(树状数组):
class Solution {
static int MAXN = 20001;
static int addNum = 10001; //将所有负数转变为正数
static int[] tree = new int[MAXN]; //tree中存储的是出现频次
public void update(int idx, int val) {
for (int i = idx; i < MAXN; i += lowbit(i)) {
tree[i] += val;
}
}
public int query(int n) {
int res = 0;
for (int i = n; i > 0; i -= lowbit(i)) {
res += tree[i];
}
return res;
}
public int lowbit(int n) {return n & (-n);}
public List<Integer> countSmaller(int[] nums) {
int len = nums.length;
List<Integer> res = new ArrayList<Integer>();
//从前往后遍历,第一个数除去自身之后,所有比它小的数的个数即是它的结果。
//每查询完自己就在树状数组中减掉自己的频次,
//这样每次都是在整个树状数组范围内查找当前值的前一个位置的前缀和.
for (int i = 0; i < len; i++) {
update(nums[i]+addNum, 1);
}
for (int i = 0; i < len; i++) {
update(nums[i]+addNum, -1);
res.add(query(nums[i]+addNum-1)); //查询的是前一个位置的前缀和。
}
return res;
}
}
最后关于归并排序和二叉搜索树的解法就不重复造轮子了,可参考彼得·攀的小站。