-
如题:
分析思路:
- 思路一:挨个元素利用
for
循环计算出后面小于当前元素数,但是这样属于两层for
循环,时间复杂度是O(N^2),不可取 - 思路二:由于我们要计算每个元素之后小于当前元素的元素个数,那么我们肯定要遍历每个元素,因此这里的O(N)的时间复杂度是至少需要的,剩下的问题是如何统计当前元素右侧更小的元素个数,那么就需要将遍历到的元素和其他元素进行比较,我们如果对每个元素都用
for
循环完成两两比较那么复杂度是O(N^2),因此这样直接整体统计的方法不可取,我们可以先部分再整体:我们不用一次性计算出当前元素右侧更小的元素个数,可以先统计一部分,最后完成整体统计,因此需要思考有什么方法在遍历数组的时候可以先将一部分元素两两比较,最后完成整体比较的?二路归并排序。- 二路归并排序会先将左边的排好序,再将右边的排好序,然后利用
merge
函数将左右两边的子数组合并,注意:我们在merge
合并的时候其实就已经对两个子数组中的元素两两比较了。等到归并排序结束,我们就能够两两比较整个数组。 - 二路归并排序时间复杂度是
nlogn
,其中logn
代表merge
函数对左右子数组合并的次数,n
代表每次合并操作的复杂度。因此我们可以理解为merge
函数的时间复杂度为O(n)
,我们会调用logn
次merge
函数。- 意思就是:merge函数里面遍历两个子数组将它们合并的复杂度是
O(n)
,此时如果我们在merge
函数中 额外 进行我们自己需要的遍历,保证我们新增的遍历操作时间复杂度不超过O(n)
,这样加上原本的合并操作,时间复杂度最多也是O(2n)
,忽略常数后时间复杂度不变。
- 意思就是:merge函数里面遍历两个子数组将它们合并的复杂度是
- 因此可以得出结论:我们可以利用归并排序中
merge
函数仅调用logn
次就能两两比较整个数组的特点,在merge函数组增加一个时间复杂度为O(n)
的遍历操作,这样就能够避免嵌套for循环的n²
时间复杂度。
- 二路归并排序会先将左边的排好序,再将右边的排好序,然后利用
- 由于最后返回的时候我们要在每个元素原来的位置记录,而排序算法会打乱元素的原始位置,因此应该记录每个元素最开始的索引位置,这里有两种思路
- 一:使用
HashMap
记录每个元素值到索引的映射,这种方法能简单快速得到每个元素的原始索引,但是只适用于数组中元素不重复的情况 - 二:我们额外使用一个记录类,这个类里面有
value
和index
两个属性,记录每个元素和该元素原始位置 - 本题应该用思路二,因为元素有重复, 需要注意的是,我们应该将原本对
int
数组排序改成对这个记录类数组排序,归并排序的时候操作是一样的,只不过需要额外调用一些value
属性罢了。
- 一:使用
代码如下:
//在归并排序的merge合并两个有序子数组的时候,如果左边大于右边,就让右指针右移,直到碰到左指针的值小于等于右指针的值,
// 此时从中间位置到右指针位置之间的所有元素就都是 小于 此时左指针的值,直接累加统计这些元素个数:right-mid-1
class Solution {
//定义一个记录类来记录数组中每个元素和原始索引
static class Record{
private int index;
private int val;
public Record(int index, int val) {
this.index = index;
this.val = val;
}
}
//由于我们的Record类里面有值,因此可以将对int数组的排序改成对Record数组的排序
private Record[] records;
//归并排序的时候需要用到的临时数组
private Record[] temp;
//记录每个元素右侧更小元素的个数
private int[] count;
public List<Integer> countSmaller(int[] nums) {
//一系列初始化
records = new Record[nums.length];
temp = new Record[nums.length];
count = new int[nums.length];
for (int i = 0; i < nums.length; i++) {
records[i] = new Record(i,nums[i]);
}
//进行归并排序
sort(records,0,nums.length-1);
//将count数组里记录的加到list集合中
List<Integer> res = new ArrayList<>();
for (int i : count){
res.add(i);
}
return res;
}
public void sort(Record[] records,int left,int right){
if(left == right){
return;
}
//这里不能用(left+right)/2,可能会有益处的风险
int mid = left + (right - left) / 2;
sort(records,left,mid);
sort(records,mid+1,right);
merge(records,left,mid,right);
}
public void merge(Record[] records,int left,int mid,int right){
//利用System.arraycopy方法将records数组中left到right位置的元素拷贝到temp数组中对应位置,
//也可以用for循环一个个赋值,但是该方法效率更高
System.arraycopy(records,left,temp,left,right-left+1);
for (int i = left,l=left,r=mid+1; i <= right; i++) {
if(l == mid+1){
records[i] = temp[r++];
}else if (r == right+1){//这里r==right+1表示右指针到头了,表示此时右边子数组中所有元素比当前元素小
count[temp[l].index] += r - mid - 1;//也可以直接写right-mid
records[i] = temp[l++];
}else if(temp[l].val <= temp[r].val){
//这里条件一定是左边小于等于右边,因为如果只写左边小于右边,
// 那么这就意味着当右边等于左边的时候指针也会右移,此时从中间到右边之中的值
// 就不全是小于左边的了,换句话说我们计算右边元素比左边小的数量的时候是利用右指针每次移动都是因为右指针指向的数小于
// 左指针指向的数,这样从mid+1到right-1这些元素就都是小于左指针的元素了,因此右指针和左指针相等的话右指针不应该右移,
// 除非计算的是右边元素小于等于当前元素的数量
count[temp[l].index] += r - mid - 1;
records[i] = temp[l++];
}else {
records[i] = temp[r++];
}
}
}
}