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