[Leetcode] 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:

Given nums = [5, 2, 6, 1]

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.
Return the array [2, 1, 1, 0].


归并排序解法

解法:
这道题目比较经典的一个解法是利用mergeSort,解决逆序对的问题。
代码基本是在归并排序的基础上进行修改得到的。
在归并排序的过程中,两部分数组有序是一个很重要的性质,这代表如果左边某个元素i大于右边某个元素j,那么左边i之后的元素都大于j;同里,如果左元素i小于 右元素j,那么i小于j后面所有的元素。这个性质要好好体会。

首先为了记录每个元素的逆序数并返回一个数组,我们对输入数组的索引进
行排序。

在归并排序中,两部分进行归并的时候,因为两部分都是有序的。用一个count记录逆序数,如果右边的元素小,在排序的同时将count加1。

如果左边元素小于右边元素,那么该元素对应的位置的逆序数记录为count。

当右边元素用完时候,将左边元素每个记录都加上count,同时排序。
左边元素用完时,排序即可,记录值不变。

可以发现,在merge时count是递增的,这就是因为如果右边元素j小于左边元素i,不仅(i,j)是一个逆序对,同时(i后面的元素,j)也是一个逆序对。

代码如下:

public class Solution {
    public List<Integer> countSmaller(int[] nums) {
        int[] result = new int[nums.length];
        int[] index = new int[nums.length];
        //对索引排序。
        for (int i = 0;i<index.length;i++) {
            index[i] = i;
        }
        mergeSort(nums,index,result,0,nums.length - 1);
        ArrayList<Integer> resultList = new ArrayList<>();
        for (int r : result) resultList.add(r);
        return resultList;
    }
    //自上而下归并排序
    private void mergeSort(int[] nums, int[] index, int[] result, int lo, int hi) {
        if (lo >= hi) return;
        int mid = lo + (hi - lo) / 2;
        mergeSort(nums, index, result, lo, mid);
        mergeSort(nums, index, result, mid + 1, hi);
        merge(nums,index,result,lo,mid,hi);
    }
    //关键函数在merge
    private void merge(int[] nums, int[] index, int[] result, int lo, int mid, int hi) {
        int[] newIndexes = new int[hi - lo + 1];
        int i = lo,j=mid + 1,rightCount = 0;
        for (int k = lo;k<=hi;k++) {
        //左边用完,右边的数不存在逆序对(因为在右边)
            if (i > mid) newIndexes[k] = index[j++];
            else if (j > hi){
            //右边用完。因为归并中两部分数组有序,之前记录的rightCount,对于左边剩余元素均使用。
                newIndexes[k] = index[i];
                result[index[i]] += rightCount;
                i++;
            }else if (nums[index[i]] > nums[index[j]]){
            //右边元素小,多出一个逆序数。
                rightCount++;
                newIndexes[k] = index[j];
                j++;
            } else if (nums[index[i]] <= nums[index[j]]) {
            //左边元素小,记录之前新生成的逆序数。
                result[index[i]] += rightCount;
                newIndexes[k] = index[i];
                i++;
            }
        }
        for (int k = lo;k<=hi;k++) {
            index[k] = newIndexes[k];
        }
    }
}

但是,在leetcode oj的时候发生了TLE。把merge函数改为while形式,时间从200ms降到了10ms。

    private void merge(int[] nums, int[] indexes, int[] count, int start, int mid, int end) {
        int[] newIndexes = new int[end - start + 1];
        int left_index = start;
        int right_index = mid+1;
        int rightcount = 0;
        int[] new_indexes = new int[end - start + 1];

        int sort_index = 0;
        while(left_index <= mid && right_index <= end){
            if(nums[indexes[right_index]] < nums[indexes[left_index]]){
                new_indexes[sort_index] = indexes[right_index];
                rightcount++;
                right_index++;
            }else{
                new_indexes[sort_index] = indexes[left_index];
                count[indexes[left_index]] += rightcount;
                left_index++;
            }
            sort_index++;
        }
        while(left_index <= mid){
            new_indexes[sort_index] = indexes[left_index];
            count[indexes[left_index]] += rightcount;
            left_index++;
            sort_index++;
        }
        while(right_index <= end){
            new_indexes[sort_index++] = indexes[right_index++];
        }
        for(int i = start; i <= end; i++){
            indexes[i] = new_indexes[i - start];
        }
    }

如果是面试的过程,个人更倾向于for循环写法,毕竟逻辑清晰。


二插搜索树解法

从后向前遍历输入数组,并构建一颗二插搜索树,在树每个节点记录数组值,和比当前根节点小的数的个数smallCount。
每插入一个节点,会判断其和根节点的大小,如果新的节点值小于根节点值,则其会插入到左子树中,我们此时要增加根节点的smallCount.
大于和等于根节点的时候,插入右子树,并将preSum和smallCount传过去,同时如果新节点大于根节点还有加上1。
需要注意的是,仅在当前根节点插入一个比根节点值小的数时才会更新smallCount。比如数组2,0,1,二叉树构建后如下:

        1(1)
        /  \
      0(0) 2(0)

可以看到虽然2右边有两个比1小的数,但是记录的smallCount是0。这是因为smallCount针对的是当前子树根节点,比1到比2小的数是0,计算结果实际上是存储在preSum中的。
假如插入一个新的3,首先,加上小于1的数(1),3>1,那么加上一个1表明将1计入结果,在到根节点2,发现没有大于1,小于2的数(记录的smallCount)是0,有3>2,加上一个1,所以最终结果是3。
计算过程类似:小于3的数 = 小于1的数+根节点1+大于1小于2的数+根节点2 = 3。
那么再插入一个3,实际只是少计算一个根节点数目而已,与插入右子树类似。
代码如下:

public class Solution {
    class Node{
        int val;
        int smalCount;
        Node left;
        Node right;
        public Node(int val, int count) {
            this.val = val;
            this.smalCount = count;
        }
    }
    public List<Integer> countSmaller(int[] nums) {
        Integer[] result = new Integer[nums.length];
        Node root = null;
        for (int i = nums.length - 1;i>=0;i--) {
            root = insert(root, nums, result, i, 0);
        }
        return Arrays.asList(result);
    }

    private Node insert(Node root, int[] nums,Integer[] result, int i, int preCount) {
        if (root == null){
            root = new Node(nums[i],0);
            result[i] = preCount;
        } else if (root.val > nums[i]){
            root.smalCount++;
            root.left = insert(root.left, nums, result, i, preCount);
        }else {
            root.right = insert(root.right, nums, result, i, preCount + root.smalCount+(root.val < nums[i]?1:0));
        }
        return root;
    }
}

这题之后,还有一套类似可以采用merge sort去解决的题,也是一道很好的题目,连接如下:
Count of range sum

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值