本篇文章为LeetCode数组模块关于分治内容的刷题笔记,仅供参考。
分治算法,即“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解(即终止条件),原问题的解即子问题的解的合并。
分治算法考虑的过程为:对于数组nums[left…right],将其分为nums[left…mid]、nums[mid+1…right]两个部分,再将原问题构建成两个子数组上的子问题以及相似问题的和。然后考虑终止条件的特殊情况即可,本质上是一种递归。
Leetcode912.排序数组
给你一个整数数组 nums,请你将该数组升序排列。
示例 1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例 2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
提示:
1 <= nums.length <= 5*104
-5*104 <= nums[i] <= 5*104
归并排序是最基本的分治法应用。
class Solution {
public:
void merge_sort(vector<int>& nums,int left,int right){
if(left>=right) return;
int mid=(left+right)/2;
merge_sort(nums,left,mid);
merge_sort(nums,mid+1,right);
vector<int> tmp(right-left+1);
int ptr1=left,ptr2=mid+1;
int pos=0;
while(ptr1<=mid && ptr2<=right){
if(nums[ptr1]<=nums[ptr2]){
tmp[pos++]=nums[ptr1++];
}else{
tmp[pos++]=nums[ptr2++];
}
}
while(ptr1<=mid){
tmp[pos++]=nums[ptr1++];
}
while(ptr2<=right){
tmp[pos++]=nums[ptr2++];
}
for(int i=left;i<=right;i++){
nums[i]=tmp[i-left];
}
return;
}
vector<int> sortArray(vector<int>& nums) {
int n=nums.size();
merge_sort(nums,0,n-1);
return nums;
}
};
Leetcode53.最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [5,4,-1,7,8]
输出:23
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
法一:对于数组nums[left…right],则其最大子数组和的取值只有以下三种情况:
其中前两种情况可以递归调用求解,第3种情况向两侧寻找最大值即可,复杂度为O(n)。因此算法的复杂度为T(n)=O(n)+2*T(n/2),即T(n)=O(nlogn)。
class Solution {
public:
int MaxNum(int a,int b,int c){
if(a>b){
if(a>c) return a;
else return c;
}
else{
if(b>c) return b;
else return c;
}
}
int CrossMid(int left,int right,vector<int>& nums){
int Suml=0;
int Maxl=0;
int Sumr=0;
int Maxr=0;
int mid=(left+right)/2;
for(int i=mid-1;i>=left;i--){
Suml+=nums[i];
if(Suml>Maxl) Maxl=Suml;
}
for(int i=mid+1;i<=right;i++){
Sumr+=nums[i];
if(Sumr>Maxr) Maxr=Sumr;
}
return nums[mid]+Maxl+Maxr;
}
int maxSub(int left,int right,vector<int>& nums){
if(left==right) return nums[left];
int mid=(left+right)/2;
int Maxl=maxSub(left,mid,nums);
int Maxr=maxSub(mid+1,right,nums);
int Maxm=CrossMid(left,right,nums);
return MaxNum(Maxl,Maxr,Maxm);
}
int maxSubArray(vector<int>& nums) {
int left=0;
int right=nums.size()-1;
return maxSub(left,right,nums);
}
};
时间复杂度过高,用时较长。
法二:下采用动态规划求解:
假设用 f(i) 代表以第 i 个数结尾的最大子数组和,那么显然:ans=max{f(i)} (0<=i
因此我们只需要求出每个位置的 f(i),然后返回 f(i) 中的最大值即可。因为 f(i) 肯定要么连着 f(i-1) ,要么单独取nums[i],即:
f(i)=max{f(i−1)+nums[i],nums[i]}
时间复杂度为O(n):
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int MaxSum=-10001;
int CurMax=0;
for(int i=0;i<nums.size();i++){
if(CurMax>0){
CurMax+=nums[i];
}
else{
CurMax=nums[i];
}
if(MaxSum<CurMax) MaxSum=CurMax;
}
return MaxSum;
}
};
法三:考虑更加高效的分治算法(力扣官方题解):
第一种分治算法的问题在于搜索 Maxm 时复杂度太高。对于数组 nums[left…right],维护4个变量:
则有:
lSum[left,right]=max{lSum[left,mid],Sum[left,mid]+lSum[mid+1,right]}
rSum[left,right]=max{rSum[left,right],Sum[mid+1,right]+rSum[left,mid]}
mSum[left,right]=max{mSum[left,mid],mSum[mid+1,right],rSum[left,mid]+lSum[mid+1,right]}
Sum[left,right]=Sum[left,mid]+Sum[mid+1,right]
代码如下:
class Solution {
public:
struct MaxInf{
int lSum,rSum,mSum,Sum;
};
MaxInf GetAns(int left,int right,vector<int>& nums){
int mid=(left+right)/2;
if(left==right){
return MaxInf{nums[left],nums[left],nums[left],nums[left]};
}
MaxInf arrl=GetAns(left,mid,nums);
MaxInf arrr=GetAns(mid+1,right,nums);
int ls=max(arrl.lSum,arrl.Sum+arrr.lSum);
int rs=max(arrr.rSum,arrr.Sum+arrl.rSum);
int ms=max(max(arrl.mSum,arrr.mSum),arrl.rSum+arrr.lSum);
int s=arrl.Sum+arrr.Sum;
return MaxInf{ls,rs,ms,s};
}
int maxSubArray(vector<int>& nums) {
int left=0;
int right=nums.size()-1;
return GetAns(left,right,nums).mSum;
}
};
Leetcode215.数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
提示:
1 <= k <= nums.length <= 104
-104 <= nums[i] <= 104
法一:最直接的做法就是先排序再索引,时间复杂度为O(nlogn)。 考虑一种可以缩短时间的排序:堆排序,因为只用删除k-1次堆顶元素,时间复杂度为O(n+klogn)=O(nlogn),其中建堆花费O(n),每次删除元素花费O(logn)。可以用现成的priority_queue
,也可以自己建堆:
class Solution {
public:
void siftdown(vector<int>& nums,int pos,int len){
while(pos>=0 && pos<len/2){
int lc=2*pos+1;
int rc=2*pos+2;
if(rc<len&&nums[rc]>=nums[lc]){
lc=rc; //寻找较大的元素
}
if(nums[pos]>=nums[lc]) return;
else{
int tmp=nums[pos];
nums[pos]=nums[lc];
nums[lc]=tmp;
pos=lc;
}
}
}
void buildHeap(vector<int>& nums){
for(int i=nums.size()/2-1;i>=0;i--){
siftdown(nums,i,nums.size());
// for(int i=0;i
// cout<
}
}
void removefirst(vector<int>& nums,int len){
int tmp=nums[len-1];
nums[len-1]=nums[0];
nums[0]=tmp;
if(len!=0) siftdown(nums,0,len-1); //此时len已经缩短
}
int findKthLargest(vector<int>& nums, int k) {
buildHeap(nums);
for(int i=0;i<k-1;i++){
removefirst(nums,nums.size()-i);
}
return nums[0];
}
};
法二:其实还有更简单的做法(来源 力扣官方解答):
回想快速排序中每次找到一个 轴值pivot 作为分界,将小于轴值的放在其左边,大于轴值的放在其右边,并不断递归。寻找第K个最大元素也可以利用该思想:只要某次划分的轴值为倒数第K个下标q的时候,我们就得到了答案。 至于 nums[l⋯q−1] 和 nums[q+1⋯r] 是否是有序的,不在考虑范围内。
因此可以改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分:
算法的时间复杂度为O(n),递归使用栈空间,空间复杂度为O(logn):
class Solution {
public:
int partition(vector<int>& nums,int l,int r,int pivot){
//将轴值前后元素归位并返回轴值下标
while(l<r){
while(nums[++l]<pivot);
while((l<r)&&(nums[--r]>pivot));
int tmp=nums[l];
nums[l]=nums[r];
nums[r]=tmp;
}
return l;
}
int quickSelect(vector<int>& nums,int l,int r,int target){
if(l>=r) return nums[l];
int q=(l+r)/2; //取轴值
int pivot=nums[q];
nums[q]=nums[r]; //将轴值置于r处
nums[r]=pivot;
q=partition(nums,l-1,r,pivot);
nums[r]=nums[q]; //将轴值放回q处
nums[q]=pivot;
if(q==nums.size()-target) return pivot;
else if(q<nums.size()-target){
return quickSelect(nums,q+1,r,target);
}
else{
return quickSelect(nums,l,q-1,target);
}
}
int findKthLargest(vector<int>& nums, int k) {
return quickSelect(nums,0,nums.size()-1,k);
}
};
Leetcode395.至少有 K 个重复字符的最长子串
给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。
示例 1:
输入:s = “aaabb”, k = 3
输出:3
解释:最长子串为 “aaa” ,其中 ‘a’ 重复了 3 次。
示例 2:
输入:s = “ababbc”, k = 2
输出:5
解释:最长子串为 “ababb” ,其中 ‘a’ 重复了 2 次, ‘b’ 重复了 3 次。
提示:
1 <= s.length <= 104
s 仅由小写英文字母组成
1 <= k <= 105
(1)对于一个字符串 s, 如果其中某个字符 c 的出现频率小于k,那么任何满足条件的包含k个重复字符的最长子串一定不会包含 c,即可以以 c 作分割,将其分为前后两个子串再进行递归。
(2)函数遍历字符串,统计每个字符的频率,以频率小于k的字符作分割,分解为多个子串,递归求解各子串的含k个重复字符的最长子串结果,然后取max即可。
需要注意的是:
"aaa",3
,"aabcccba",2
这样完全符合的样例得到的结果却是0:bool split=false; //判断是否需要分割
for(int i=left;i<=right;i++){
if(letter[s[i]-'a']<k) split=true;
}
if(!split) return len;
需要处理的特殊情况为:
class Solution {
public:
int LongestS(string& s,int k,int left,int right){
int len=right-left+1;
if(len<k) return 0; //包含了len<=0
int letter[26]={0};
for(int i=left;i<=right;i++){ //统计字符频率
letter[s[i]-'a']++;
}
bool split=false; //判断是否需要分割
for(int i=left;i<=right;i++){
if(letter[s[i]-'a']<k) split=true;
}
if(!split) return len;
int ptr1=left-1,ptr2=left-1;
int Maxlen=0;
for(int i=left;i<=right;i++){ //递归子串
if(letter[s[i]-'a']<k){
ptr1=ptr2;
ptr2=i;
int tmp=LongestS(s,k,ptr1+1,ptr2-1);
Maxlen=Maxlen>tmp ? Maxlen:tmp;
}
}
int tmp=LongestS(s,k,ptr2+1,right);
Maxlen=Maxlen>tmp ? Maxlen:tmp; //别遗漏s[ptr2+1...right]
return Maxlen;
}
int longestSubstring(string s, int k) {
if(k==1) return s.size();
else return LongestS(s,k,0,s.size()-1);
}
};
Leetcode4.寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n)) 。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
提示:
nums1.length = m
nums2.length = n
0 <= m <= 1000
0 <= n <= 1000
1 <= m + n <= 2000
-106 <= nums1[i], nums2[i] <= 106
法一:直接思路是归并排序中的“合”的过程:两个指针分别指向nums1、nums2的开头,每次循环将小的元素放入结果数组,若某个数组为空,则将另一个剩下的元素全部放入。注意讨论nums1或nums2为空的情况。
时间复杂度为O(m+n),空间复杂度为O(m+n):
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m=nums1.size();
int n=nums2.size();
vector<int> SortedV(m+n);
if(m==0){
if(n%2==1) return nums2[n/2];
else return (nums2[n/2-1]+nums2[n/2])/2.0;
}
else if(n==0){
if(m%2==1) return nums1[m/2];
else return (nums1[m/2-1]+nums1[m/2])/2.0;
}
int ptr1=0;
int ptr2=0;
int cnt=0;
while(cnt<(m+n)){
if(ptr1==m){ //nums1到头
while(ptr2<n){
SortedV[cnt]=nums2[ptr2];
cnt++;
ptr2++;
}
break;
}
else if(ptr2==n){ //nums2到头
while(ptr1<m){
SortedV[cnt]=nums1[ptr1];
cnt++;
ptr1++;
}
break;
}
if(nums1[ptr1]<nums2[ptr2]){
SortedV[cnt]=nums1[ptr1];
cnt++;
ptr1++;
}
else{
SortedV[cnt]=nums2[ptr2];
cnt++;
ptr2++;
}
}
if(cnt%2==1){
return SortedV[cnt/2];
}
else{
return (SortedV[cnt/2-1]+SortedV[cnt/2])/2.0;
}
}
};
法二:其实题目只是让找到中位数,并不需要将结果存入数组,因此可以将上述的空间复杂度降为O(1):
双指针遍历nums1、nums2,元素小的指针向前进1,直到遍历过的元素数量为 (m+n)/2。此时需要分类讨论 m+n 的奇偶性,以及兼顾某一个指针在开头没动或者提前到达末尾等诸多情况。因为自己写的代码像一坨*山,此处参考 windliang 大佬的代码并稍作修改:
(m+n)/2+1
次:因为对于奇数的情况,需要知道第 (m+n)//2 个数,即遍历(m+n)/2+1
次;对于偶数的情况,需要知道第 (m+n)/2-1 和 (m+n)/2 个数,也是遍历(m+n)/2+1
次。pre
、cur
记录每次的结果,即可避免讨论最终返回的数值。if(ptr1<m && (ptr2>=n||nums1[ptr1]<nums2[ptr2]){
cur=nums1[ptr1++];
}else{
cur=nums2[ptr2++];
}
算法时间复杂度O(m+n),空间复杂度O(1):
class Solution {
public:
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m=nums1.size();
int n=nums2.size();
int ptr1=0,ptr2=0;
int pre=0,cur=0;
for(int i=0;i<(m+n)/2+1;i++){
pre=cur;
if(ptr1<m && (ptr2>=n||nums1[ptr1]<nums2[ptr2])){
cur=nums1[ptr1++];
}else{
cur=nums2[ptr2++];
}
}
if((m+n)%2==0) return (pre+cur)/2.0;
else return cur;
}
};
法三:其实上述解法是一个一个排除位于中位数之前的元素,考虑到数组的有序性,我们还可以批量排除,即通过二分一半一半地排除。将问题转化为寻找两个有序数组中的第 k 小的数(即顺序数组中的nums[k-1]),其中 k 为 (m+n)/2+1 和 (m+n)/2(偶数情况下),每次排除 k/2i 个元素。
要找到第 k 个元素,我们可以分别找到 nums1 和 nums2 中第 k/2 个元素:比较 nums1[k/2−1] 和 nums2[k/2−1],由于 nums1[k/2−1] 和 nums2[k/2−1] 的前面分别各有 k/2−1 个元素,对于 nums1[k/2−1] 和 nums2[k/2−1] 中的较小值,最多只会有 (k/2-1)+(k/2-1)≤k−2 个元素比它小,即 较小值最大也只能是第 k-1 小的数,那么它就不能是第 k 小的数了。此处较小值相当于快排中的轴值。
因此我们可以归纳出以下情况:
可以看到,比较 nums[k/2−1] 和 nums2[k/2−1] 之后,可以排除 k/2 个不可能是第 k 小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少 k 的值,这是因为我们排除的数都不大于第 k 小的数。
有以下三种情况需要特殊处理:
if(len1>len2) return findKth(nums2,nums1,start2,start1,k)
,保证若有数组空了一定是nums1,思路值得借鉴;class Solution {
public:
int min(int a,int b){
return a<b ? a:b;
}
int findKth(vector<int>& nums1,vector<int>& nums2,int start1,int start2,int k){
int m=nums1.size()-start1; //nums1[start1...m-1]
int n=nums2.size()-start2; //nums2[start2...n-1]
//处理特殊情况
if(m==0) return nums2[start2+k-1];
else if(n==0) return nums1[start1+k-1];
else if(k==1) return min(nums1[start1],nums2[start2]);
//保证数组不越界需要对k/2和数组长度取小
int ptr1=start1+min(k/2,m)-1; //nums1[k/2-1]
int ptr2=start2+min(k/2,n)-1;
if(nums1[ptr1]>nums2[ptr2]){
return findKth(nums1,nums2,start1,ptr2+1,k-(ptr2-start2+1));
}
else{
return findKth(nums1,nums2,ptr1+1,start2,k-(ptr1-start1+1));
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int m=nums1.size();
int n=nums2.size();
//第k大元素在数组中下标为k-1
if((m+n)%2==1) return findKth(nums1,nums2,0,0,(m+n+1)/2);
else return (findKth(nums1,nums2,0,0,(m+n)/2)+findKth(nums1,nums2,0,0,(m+n)/2+1))/2.0;
}
};
剑指 Offer 51.数组中的逆序对
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
示例 1:
输入: [7,5,6,4]
输出: 5
限制:
0 <= 数组长度 <= 50000
法一:逆序对的本质是(较大元素,较小元素),不妨每次直接找到数组的最大值 max,那么 max 与它前面的数都构不成逆序对,max 与其后面的值都可以构成逆序对,记录 n-index-1
后将 max 删除,并继续递归。
需要注意的是,数组中有可能有重复元素,因此 max 要找数组中最后一个,即判断时 if(nums[i]>=max)
一定要挂等号。
class Solution {
public:
int swap(vector<int>& nums,int n){
if(n==0||n==1) return 0;
int max=INT_MIN;
int index;
for(int i=0;i<n;i++){
if(nums[i]>=max){ //这里挂等号以保证是最后一个最大值
max=nums[i];
index=i;
}
}
nums.erase(nums.begin()+index);
return n-index-1+swap(nums,n-1);
}
int reversePairs(vector<int>& nums) {
return swap(nums,nums.size());
}
};
或者写成while循环的形式:
class Solution {
public:
int reversePairs(vector<int>& nums) {
int n=nums.size();
int cnt=0;
int index;
while(n>1){
int max=INT_MIN;
for(int i=0;i<n;i++){
if(nums[i]>=max){
max=nums[i];
index=i;
}
}
nums.erase(nums.begin()+index);
cnt+=(n-index-1);
n--;
}
return cnt;
}
};
法二:考虑将问题用分治法解决:
(1)将数组分为nums[left…mid]和nums[mid+1…right],于是逆序对由以下3个部分组成:
(2)下面讨论如何解决前后两部分之间的逆序对数:
每次递归完nums[left…mid]和nums[mid+1…right]后,不妨将其排序,对于有序数组,逆序对的处理则有规律可循。
双指针ptr1、ptr2索引元素进行比较,遍历 nums[left…mid] 中的每个 nums[ptr1],找到 nums[mid+1…right] 中最小的 nums[ptr2] 使得 nums[ptr1]>nums[ptr2],因为 nums[left…mid] 和 nums[mid+1…right] 都是升序排列,所以 nums[ptr1…mid]与nums[ptr2]均成逆序。又由于两段数组是升序的,因此满足 nums[ptr1+1]>nums[ptr2] 的 ptr2 一定不小于满足 nums[ptr1]>nums[ptr2] 的 ptr2,即 ptr2 每次不用重置,向后遍历即可。整理一下思路:
ptr1初值为left,ptr2初值为mid+1,双指针在while循环中交替向后,
逆序对数+=mid-ptr1+1
;此时与 nums[ptr2] 相关的逆序对已经全部计算出,因此ptr2++
;ptr1++
。并且在上述过程中将两段数组按归并排序排好顺序:
class Solution {
public:
int re_Pairs(vector<int>& nums,int left,int right){ //求解两段数组间的逆序对并各自排序
int mid=(left+right)/2;
vector<int> tmp(right-left+1); //中间数组,暂时存放排好序的数组
int cnt=0;
int ptr1=left,ptr2=mid+1;
int pos=0; //tmp数组中下标位置
while(ptr1<=mid && ptr2<=right){
if(nums[ptr1]>nums[ptr2]){
cnt+=mid-ptr1+1; //nums[ptr1...mid]与nums[ptr2]均成逆序
tmp[pos++]=nums[ptr2++];
}
else{
tmp[pos++]=nums[ptr1++];
}
}
while(ptr1<=mid){ //将nums[ptr1...mid]元素排序
tmp[pos++]=nums[ptr1++];
}
while(ptr2<=right){ //将nums[ptr2...right]元素排序
tmp[pos++]=nums[ptr2++];
}
for(int i=left;i<=right;i++){
nums[i]=tmp[i-left];
}
return cnt;
}
int merge(vector<int>& nums,int left,int right){
if(left>=right) return 0;
int mid=(left+right)/2;
return merge(nums,left,mid)+merge(nums,mid+1,right)+re_Pairs(nums,left,right);
}
int reversePairs(vector<int>& nums) {
return merge(nums,0,nums.size()-1);
}
};
Leetcode315.计算右侧小于当前元素的个数
给你一个整数数组 nums ,按要求返回一个新数组 counts 。数组 counts 有该性质: counts[i] 的值是 nums[i] 右侧小于 nums[i] 的元素的数量。
示例 1:
输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
示例 2:
输入:nums = [-1]
输出:[0]
示例 3:
输入:nums = [-1,-1]
输出:[0,0]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
(1)右侧小于当前元素的个数本质就是逆序对,参考上一题,考虑归并排序中“合”的过程:因为归并是将数组不断二分再相邻合并,因此对于每个元素只需要统计其“合”的过程中位于 nums[left…mid] 时相对于 nums[mid+1…right] 中的逆序对,然后随着“合”的过程不断累加,最终的结果即为右侧小于当前元素的个数。
例如对于数组[7,1,2,5,6,9,8,4]
,元素7右侧小于当前元素的个数即为:
即元素7右侧小于当前元素的个数为5。
(2)对于两段已经排好序的数组 nums[left…mid]、nums[mid+1…right],对于 nums[left…mid] 中的元素 nums[ptr1],如何求解其相对于 nums[mid+1…right] 中元素的逆序对?其实在归并排序的过程中,通过双指针合并 nums[left…mid]、nums[mid+1…right] 时,当 nums[ptr1] 要被放入辅助数组时,nums[mid+1…right] 还剩下的元素都是大于等于 nums[ptr1] 的,已经放入辅助数组中的都是比 nums[ptr1] 小的,由此可以统计出 nums[ptr1] 与其右端数组所成逆序对。
以两个有序数组[1,2,5,7],[4,6,8,9]
为例展示归并过程中统计逆序对:
当 7 被放入辅助数组时,tmp=[1,2,4,5,6]
,ptr2指向8,此时 nums[mid+1…right] 中有2个元素被压入tmp,即 nums[left…mid] 中的元素 7 相对于 nums[mid+1…right] 中元素的逆序对数为2。
需要注意的是:
nums[ptr1]==nums[ptr2]
时,需要让nums[ptr1]放入辅助数组,因为如果让nums[ptr2]放入的话,在接下来nums[ptr1]放入辅助数组时会将nums[ptr2]计算成小于nums[ptr1]的元素;index[i]
表示 nums[i]
的原始位置。class Solution {
public:
void divide(vector<int>& nums,vector<int>& index,vector<int>& ans,int left,int right){
//将nums[left...right]排序并统计逆序对
if(left>=right) return;
int mid=(left+right)/2;
divide(nums,index,ans,left,mid); //统计nums[left...mid]逆序对并排序
divide(nums,index,ans,mid+1,right);
vector<int> tmp(right-left+1);
vector<int> tmp_index(right-left+1);
int ptr1=left,ptr2=mid+1;
int pos=0;
while(ptr1<=mid && ptr2<=right){
if(nums[ptr1]<=nums[ptr2]){ //挂等号因为相等的不能计入逆序对
//nums[ptr1]放入辅助数组,在其前面放入的nums[mid+1...right]中元素都成逆序
ans[index[ptr1]]+=(ptr2-mid-1);
tmp_index[pos]=index[ptr1];
tmp[pos++]=nums[ptr1++];
}
else{
tmp_index[pos]=index[ptr2];
tmp[pos++]=nums[ptr2++];
}
}
while(ptr1<=mid){
ans[index[ptr1]]+=(right-mid);
tmp_index[pos]=index[ptr1];
tmp[pos++]=nums[ptr1++];
}
while(ptr2<=right){
tmp_index[pos]=index[ptr2];
tmp[pos++]=nums[ptr2++];
}
for(int i=left;i<=right;i++){
nums[i]=tmp[i-left];
index[i]=tmp_index[i-left];
}
return;
}
vector<int> countSmaller(vector<int>& nums) {
int n=nums.size();
vector<int> ans(n);
vector<int> index(n);
for(int i=0;i<n;i++) index[i]=i;
divide(nums,index,ans,0,n-1);
return ans;
}
};