題目:
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