微软面试题:剑指 Offer 51. 数组中的逆序对 Hard 出现次数:3
题目描述:
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4] 输出: 5
限制:
0 <= 数组长度 <= 50000
分析:
本题的暴力方法显然容易想到,但是会报超时,难度等级 hard 显示 考察的是使用
时间O(log n*logn)空间 O(n)的解法。可以使用 「归并排序」 和 「线段树」 两种方法。
利用「归并排序」和「线段树」计算逆序对都是非常经典的做法。这里我们暂且只考虑
利用「归并排序」计算逆序对。
思想是「分治算法」,所有的「逆序对」来源于 3 个部分:
- 左边区间的逆序对;
- 右边区间的逆序对;
- 横跨两个区间的逆序对。
计算左边区间的逆序对和右边区间的逆序对都是规模更小的子问题,直接交给递归去完成。
重点是分析计算横跨两个区间的逆序对。计算横跨两个区间的逆序对时,上面的两个子问题都已经让递归函数完成了,
此时,左边区间的逆序对和右边区间的逆序对都已经计算出来了,且左边区间和右边区间都已经有序了。
计算横跨两个区间的逆序对 具体步骤如下:
1. 将给定区间 nums[l,r] 分成 左区间 nums[l , mid] ,右区间 nums[mid + 1,r] ,使用双指针同步遍历左右区间;
2. 如果左边区间当前的元素num[i] 小于等于 右边区间当前的元素nums [j],因为nums[i] 小于等于右边区间
所有的元素nums[j,r],nums[i] 不会和右区间内的元素nums[j,r] 构成逆序对。直接将nums[i] 放入归并排序的辅助空间。
3. 如果左边区间当前的元素num[i] 大于 右边区间当前的元素nums [j],那左边区间元素num[i,mid] ,一共 mid - i + 1 个都比
右边区间当前的元素nums[j]大,且都在右边区间当前的元素前面,和右边区间当前元素 构成 mid - i + 1 个逆序对。
加到总的逆序对数上,再将右边区间当前的元素nums [j] 放到缓冲区。
3. 将左(右)区间比较多出来的元素直接加到辅助空间中.
4. 将辅助空间中排序好的元素 再重新放回nums[l,r];
5. 函数返回当前区间计算得到的总的逆序对数。
代码如下:
1 class Solution { 2 public: 3 vector<int> tmp;//归并排序的辅助数组 4 5 int reversePairs(vector<int>& nums) { 6 tmp.assign(nums.size(),0); 7 return merge_sort(nums,0,nums.size() - 1); 8 } 9 10 long long int merge_sort(vector<int>& nums,int l,int r) 11 { 12 if(l >= r)//归并排序递归出口 13 { 14 return 0; 15 } 16 int mid = l + (r - l)/2;//将区间一分为二 17 long long ans = merge_sort(nums,l,mid) + merge_sort(nums,mid + 1,r);//递归地求左右子数组的逆序对个数和 18 int k = 0,i = l,j = mid + 1;//计算 横跨左右区间的逆序对 19 while(i <= mid && j <= r) 20 { 21 if(nums[i] <= nums[j]) 22 { 23 tmp[k++] = nums[i++]; 24 } 25 else 26 { //执行上述递归之后,左右子数组都已经排好序 27 //nums[i:mid] 都比nums[j] 大,都和num[j] 构成逆序对 28 ans += (mid - i + 1); 29 tmp[k++] = nums[j++]; 30 } 31 } 32 while(i <= mid) tmp[k++] = nums[i++]; 33 while(j <= r) tmp[k++] = nums[j++]; 34 //将排序好的元素移回原数组,放在原来的区间上 35 for(i = l,j = 0;i <= r; ) 36 { 37 nums[i++] = tmp[j++]; 38 } 39 return ans; 40 } 41 };