[LeetCode/力扣][Java] 0315. 计算右侧小于当前元素的个数(Count of Smaller Numbers After Self)

题目描述:

给你一个整数数组 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;
    }
}

最后关于归并排序和二叉搜索树的解法就不重复造轮子了,可参考彼得·攀的小站


2022/11/24更新归并排序的代码。

此题难以下手的话可以先去做[剑指offer]#051.数组中的逆序对。归并的话做两三道题记住模板就好做多了。求逆序对问题就是在从底向上归并的过程中,每当元素被归并回去的时候,根据题目进行计算。本题在逆序对的基础上多了一个索引数组,相当于多绕了一圈,但并不难。假设初始时left1、left2 分别指向两个有序序列的第一个位置。在循环向后遍历的过程中,当left1所指向的indexes数组对应的nums数值(也就是left1指向的实际数值)小于left2指向的数值时,从第二个有序序列的第一个位置到left2的所有元素(不包括left2)均小于left1指向的数值,且这些元素均位于原数组中left1所指向的元素的右侧。此时将这些元素的个数记入counts数组中。
为什么不会重复计算?答:因为当左侧更小的元素被归并回去的时候(以cur标记),本趟归并一定不会再计算一遍该元素。当本趟归并完成后,cur所指向的元素的子序列已经是个有序序列了。当它进行下一趟归并时,与cur进行比较的元素来自于另外的子序列。而这个新的子序列所有元素也从没与cur进行过比较,因此绝对不会重复计算cur右侧小于它的元素个数。

代码4(归并排序)

class Solution {
    public  List<Integer> countSmaller(int[] nums) {
        int n = nums.length;
        int[] counts = new int[n];      //记录结果数组
        int[] indexes = new int[n];     //索引数组
        int[] temp = new int[n];    //索引辅助数组
        int left = 0;
        int right = n-1;
        for (int i = 0; i < n; i++) {
            indexes[i] = i;
            temp[i] = i;
        }
        meregeOfSmallerNum(nums, left, right, indexes, temp, counts);
        List<Integer> res = new ArrayList<Integer>();//用于返回结果
        for (int i = 0; i < n; i++) {
            res.add(counts[i]);
        }
        return res;
    }

    public void meregeOfSmallerNum(int[] nums, int left, int right, int[] indexes, int[] temp, int[] counts) {
        if (left >= right) return;
        int mid = (left + right) / 2;
        meregeOfSmallerNum(nums, left, mid, indexes, temp, counts);
        meregeOfSmallerNum(nums, mid+1, right, indexes, temp, counts);
        
        int left1 = left;
        int left2 = mid+1;
        int k = left;

        while (left1 <= mid || left2 <= right) {
            if (left1 > mid) { //左子序列为空,只剩右侧,此时不需要更新counts数组
                temp[k++] = indexes[left2];
                left2++;
            }
            else if (left2 > right) { //右子序列为空,说明剩下左子序列的值均大于右子序列,即每个数的右侧都有(right-mid)个更小的数
                temp[k++] = indexes[left1];
                counts[indexes[left1]] += (right-mid);
                left1++;
            }
            else if (nums[indexes[left1]] <= nums[indexes[left2]]) { 
                //左侧即将被归并(这个数不会再出现了),此时说明left1比已归并的右侧序列[mid+1, left2-1]都大,计算counts
                temp[k++] = indexes[left1];
                counts[indexes[left1]] += (left2-mid-1);
                left1++;
            } else {    //左侧更大,不进行计算(因为更大的数不会被归并,在此处计算会导致重复)
                temp[k++] = indexes[left2];
                left2++;
            }
        }
        for (int i = left; i <= right; i++)
            indexes[i] = temp[i];   //将新的归并索引数组更新
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值