[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

發佈了57 篇原創文章 · 獲贊 21 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章