【Leetcode】315. Count of Smaller Numbers After Self

题目地址:

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,...,n1],接着对 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[n1]],这样在归并排序的过程中,我们就能不但知道每个位置的数值,还能知道它的下标,这样就方便统计每个位置的逆序对的数量了。代码如下:

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(Mm)),空间 O ( M − m ) O(M-m) O(Mm) m m m M M M分别是数组最小和最大值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值