【Leetcode刷题】分治

本篇文章为 LeetCode 分治模块的刷题笔记,仅供参考。

分治算法,即“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解(即终止条件),原问题的解即子问题的解的合并。

分治算法考虑的过程为:对于数组 nums[left…right],将其分为 nums[left…mid]、nums[mid+1…right] 两个部分,再将原问题构建成两个子数组上的子问题以及相似问题的和。然后考虑终止条件的特殊情况即可,本质上是一种递归。

Leetcode912.排序数组

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.最大子数组和

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],则其最大子数组和的取值只有以下三种情况:

  1. 左半边子数组nums[left…mid]
  2. 右半边子数组nums[mid+1…right]
  3. 横跨mid,即为nums[i…mid…j]且left<=i<mid<j<=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<n)
因此我们只需要求出每个位置的 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个变量:

  1. lSum 表示 以left为左端点 的最大子数组和
  2. rSum 表示 以right为右端点 的最大子数组和
  3. mSum 表示 nums[left…right]内的最大子段和
  4. Sum表示数组nums[left…right]的和

则有:

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个最大元素

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<nums.size();i++)  cout<<nums[i]<<" ";
            // cout<<endl;
        }
    }
    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] 是否是有序的,不在考虑范围内。

因此可以改进快速排序算法来解决这个问题:在分解的过程当中,我们会对子数组进行划分:

  1. 如果划分得到的 q 正好就是我们需要的下标,就直接返回 nums[q];
  2. 如果 q 比目标下标小,就递归右子区间;
  3. 如果 q 比目标下标大,就递归左子区间。

算法的时间复杂度为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 个重复字符的最长子串

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即可。

需要注意的是:

  1. 统计完每个字符的频率后需要先遍历判断是否需要再分割,否则像 "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;
  1. 遍历完别忘了还有s[ptr2+1…right]没有递归

需要处理的特殊情况为:

  1. k==1:任何字符都满足,直接返回s.size()即可;
  2. s.size()<k:无论如何拼凑都无法满足,返回0。
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.寻找两个正序数组的中位数

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 大佬的代码并稍作修改:

  1. 将奇数与偶数的情况合并,双指针初始值为0,遍历(m+n)/2+1次:因为对于奇数的情况,需要知道第 (m+n)//2 个数,即遍历(m+n)/2+1次;对于偶数的情况,需要知道第 (m+n)/2-1 和 (m+n)/2 个数,也是遍历(m+n)/2+1次。
  2. 为解决某一个指针在开头没动或提前到达末尾,用两个变量记录当前和上一次循环得到的数值最终结果本质上返回的是第 (m+n)/2+1 次循环到的结果(偶数情况还有第 (m+n)/2次),因此用变量precur记录每次的结果,即可避免讨论最终返回的数值。
  3. 判断ptr1还是ptr2后移的条件:
if(ptr1<m && (ptr2>=n||nums1[ptr1]<nums2[ptr2]){
	cur=nums1[ptr1++];
}else{
	cur=nums2[ptr2++];
}
  1. 为保证函数返回值为浮点数,偶数情况返回 (pre+cur) / 2.0

算法时间复杂度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 小的数了。此处较小值相当于快排中的轴值。

因此我们可以归纳出以下情况:

  1. 如果 nums1[k/2-1] <= nums2[k/2-1],则比 nums1[k/2−1] 小的数最多只有 nums1 的前 k/2-1 个数和 nums2 的前 k/2-1 个数,即比 nums1[k/2−1] 小的数最多只有 k-2 个,因此 nums1[k/2-1] 不可能是第 k 个数,nums1[0] 到 nums1[k/2−1] 也都不可能是第 k 个数,可以全部排除;
  2. 如果 nums1[k/2−1]>nums2[k/2−1],则可以排除 nums2[0] 到 nums2[k/2-1]。

可以看到,比较 nums[k/2−1] 和 nums2[k/2−1] 之后,可以排除 k/2 个不可能是第 k 小的数,查找范围缩小了一半。同时,我们将在排除后的新数组上继续进行二分查找,并且根据我们排除数的个数,减少 k 的值,这是因为我们排除的数都不大于第 k 小的数。

有以下三种情况需要特殊处理:

  1. 如果 nums1[k/2−1] 或者 nums2[k/2−1] 越界,那么我们应当选取对应数组中的最后一个元素。在这种情况下,我们必须根据排除数的个数减少 k 的值,而不能直接将 k 减去 k/2;
  2. 如果一个数组为空,说明该数组中的所有元素都被排除,我们可以直接返回另一个数组中第 k 小的元素。看 windliang 大佬为了方便逻辑运算,递归前加入判断if(len1>len2) return findKth(nums2,nums1,start2,start1,k),保证若有数组空了一定是nums1,思路值得借鉴;
  3. 如果 k=1,我们只要返回两个数组首元素的最小值即可。
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.数组中的逆序对

剑指 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个部分组成

  1. nums[left…mid]中逆序对,可以用递归解决
  2. nums[mid+1…right]中逆序对,也可以用递归解决
  3. nums[left…mid]和nums[mid+1…right]两部分之间的逆序对

(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循环中交替向后,

  1. 若 nums[ptr1]>nums[ptr2],则 nums[ptr1] 后面的元素与 nums[ptr2] 均可以构成逆序对,逆序对数+=mid-ptr1+1;此时与 nums[ptr2] 相关的逆序对已经全部计算出,因此ptr2++
  2. 若 nums[ptr1]<=nums[ptr2],此时与 nums[ptr1] 相关的逆序对已经全部算出,因此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.计算右侧小于当前元素的个数

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右侧小于当前元素的个数即为:

  1. [7,1,2,5] 中元素 7 相对于 [6,9,8,4] 中元素的逆序对数:2
  2. [7,1] 中元素 7 相对于 [2,5] 中元素的逆序对数:2
  3. [7] 中元素 7 相对于 [1] 中元素的逆序对数:1

即元素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。

需要注意的是

  1. nums[ptr1]==nums[ptr2]时,需要让nums[ptr1]放入辅助数组,因为如果让nums[ptr2]放入的话,在接下来nums[ptr1]放入辅助数组时会将nums[ptr2]计算成小于nums[ptr1]的元素;
  2. 由于在归并过程中元素的位置被交换,因此需要引入数组记录下标,在排序同时记录下标: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;
    }
};
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值