【优选算法之分治】No.9--- 经典分治算法


前言

在这里插入图片描述

👧个人主页:@小沈YO.
😚小编介绍:欢迎来到我的乱七八糟小星球🌝
📋专栏:优选算法
🔑本章内容:分治
记得 评论📝 +点赞👍 +收藏😽 +关注💞哦~


一、分治- - -快排 示例:

1.1 颜⾊分类

  1. 题⽬链接:75. 颜⾊分类
  2. 题⽬描述:
    在这里插入图片描述
  3. 解法(快排思想 - 三指针法使数组分三块):
    算法思路:
    类⽐数组分两块的算法思想,这⾥是将数组分成三块,那么我们可以再添加⼀个指针,实现数组分三块。
    设数组⼤⼩为 n ,定义三个指针 left, cur, right :
    ◦ left :⽤来标记 0 序列的末尾,因此初始化为 -1 ;
    ◦ cur :⽤来扫描数组,初始化为 0 ;
    ◦ right :⽤来标记 2 序列的起始位置,因此初始化为 n 。
    在 cur 往后扫描的过程中,保证:
    ◦ [0, left] 内的元素都是 0 ;
    ◦ [left + 1, cur - 1] 内的元素都是 1 ;
    ◦ [cur, right - 1] 内的元素是待定元素;
    ◦ [right, n] 内的元素都是 2 。
    算法流程:
    a. 初始化 cur = 0,left = -1, right = numsSize ;
    b. 当 cur < right 的时候(因为 right 表⽰的是 2 序列的左边界,因此当 cur 碰到 right 的时候,说明已经将所有数据扫描完毕了),⼀直进⾏下⾯循环: 根据 nums[cur] 的值,可以分为下⾯三种情况:
    <1> nums[cur] = = 0 ;说明此时这个位置的元素需要在 left + 1 的位置上,因此交换 left + 1 与 cur 位置的元素,并且让 left++ (指向 0 序列的右边界),cur++ (为什么可以 ++ 呢,是因为 left + 1 位置要么是 0 ,要么是 cur ,交换完毕之后,这个位置的值已经符合我们的要求,因此 cur++ );
    <2> nums[cur] = = 1 ;说明这个位置应该在 left 和 cur 之间,此时⽆需交换,直接让 cur++ ,判断下⼀个元素即可;
    <3> nums[cur] == 2 ;说明这个位置的元素应该在 right - 1 的位置,因此交换 right - 1 与 cur 位置的元素,并且让 right-- (指向 2 序列的左边界),cur 不变(因为交换过来的数是没有被判断过的,因此需要在下轮循环中判断)
    c. 当循环结束之后:
    [0, left] 表⽰ 0 序列;
    [left + 1, right - 1] 表⽰ 1 序列;
    [right, numsSize - 1] 表⽰ 2 序列。
  4. C++代码
class Solution {
public:
    void sortColors(vector<int>& nums) 
    {
        int left=-1,right=nums.size(),cur=0;
        while(cur<right)
        {
            if(nums[cur]==0)swap(nums[cur++],nums[++left]);
            else if(nums[cur]==1)cur++;
            else swap(nums[cur],nums[--right]);
        }
    }
};

1.2 排序数组

  1. 题⽬链接:912. 排序数组
    由于⼒扣的测试⽤例在不断加强,所以这⾥的数组划分三块的思想搭配随机选择基准元素的⽅法是⽐较优秀的。
  2. 题⽬描述:
    在这里插入图片描述
  3. 解法(数组分三块思想 + 随机选择基准元素的快速排序):
    算法思路:
    我们在数据结构阶段学习的快速排序的思想可以知道,快排最核⼼的⼀步就是 Partition (分割数据):将数据按照⼀个标准,分成左右两部分。
    如果我们使⽤荷兰国旗问题的思想,将数组划分为 左 中 右 三部分:左边是⽐基准元素⼩的数据,中间是与基准元素相同的数据,右边是⽐基准元素⼤的数据。然后再去递归的排序左边部分和右边部分即可(可以舍去⼤量的中间部分)。在处理数据量有很多重复的情况下,效率会⼤⼤提升。
    算法流程:
    随机选择基准算法流程:
    函数设计:int randomKey(vector& nums, int left, int right)
    a. 在主函数那⾥种⼀颗随机数种⼦;
    b. 在随机选择基准函数这⾥⽣成⼀个随机数;
    c. 由于我们要随机产⽣⼀个基准,因此可以将随机数转换成随机下标:让随机数 % 上区间⼤⼩,然后加上区间的左边界即可。
    快速排序算法主要流程:
    a. 定义递归出⼝;
    b. 利⽤随机选择基准函数⽣成⼀个基准元素;
    c. 利⽤荷兰国旗思想将数组划分成三个区域;
    d. 递归处理左边区域和右边区域。
  4. C++代码
class Solution {
public:
    void Sort(int l,int r,vector<int>& nums)
    {
        if(l>=r)return;
        int cur=l,left=l-1,right=r+1;
        int ret=getRandom(nums,l,r);
        //int ret=rand()%(r-l+1)+l;
        //int ret=nums[rand()%(r-l+1)+l];
        while(cur<right)
        {
            if(nums[cur]<ret)swap(nums[++left],nums[cur++]);
            //if(nums[cur]<nums[ret])swap(nums[++left],nums[cur++]);//这里不可以这样写哦因为nums是一直变动的
            else if(nums[cur]==ret)cur++;
            //else if(nums[cur]==nums[ret])cur++;
            else swap(nums[--right],nums[cur]);
        }
        Sort(l,left,nums);
        Sort(right,r,nums);
    }
    int getRandom(vector<int>& nums, int left, int right)
    {
        int r = rand();
        return nums[r % (right - left + 1) + left];
    }
    vector<int> sortArray(vector<int>& nums) 
    {
        srand(time(NULL));
        Sort(0,nums.size()-1,nums);
        return nums;
    }
};

1.3 数组中的第K个最⼤元素

  1. 题⽬链接:215. 数组中的第K个最⼤元素
  2. 题⽬描述:
    在这里插入图片描述
  3. 解法(快速选择算法):
    算法思路:
    在快排中,当我们把数组「分成三块」之后: [l, left] [left + 1, right - 1][right, r] ,我们可以通过计算每⼀个区间内元素的「个数」,进⽽推断出我们要找的元素是在「哪⼀个区间」⾥⾯。那么我们可以直接去「相应的区间」去寻找最终结果就好了。
  4. C++代码
class Solution {
public:
    void Sort(vector<int>&nums,int l,int r)
    {
        if(l>=r)return;
        int cur=l,left=l-1,right=r+1;
        int ret=getRandom(nums,l,r);
        while(cur<right)
        {
            if(nums[cur]<ret)swap(nums[++left],nums[cur++]);
            else if(nums[cur]==ret)cur++;
            else swap(nums[--right],nums[cur]);
        }
        Sort(nums,l,left);
        Sort(nums,right,r);
    }
    int getRandom(vector<int>&nums,int l,int r)
    {
        return nums[rand()%(r-l+1)+l];
    }
    int findKthLargest(vector<int>& nums, int k) 
    {
        srand(time(NULL));
        Sort(nums,0,nums.size()-1);
        return nums[nums.size()-k];
    }
};

-------------------------------------------------------------------------------------------

class Solution {
public:
    int Sort(vector<int>&nums,int l,int r,int k)
    {
        if(l==r)return nums[l];
        int cur=l,left=l-1,right=r+1;
        int ret=getRandom(nums,l,r);
        while(cur<right)
        {
            if(nums[cur]<ret)swap(nums[++left],nums[cur++]);
            else if(nums[cur]==ret)cur++;
            else swap(nums[--right],nums[cur]);
        }
        int c=r-right+1,b=right-left-1;
        if(c>=k)return Sort(nums,right,r,k);
        else if(b+c>=k)return ret;
        else return Sort(nums,l,left,k-c-b);
    }
    int getRandom(vector<int>&nums,int l,int r)
    {
        return nums[rand()%(r-l+1)+l];
    }
    int findKthLargest(vector<int>& nums, int k) 
    {
        srand(time(NULL));
        return Sort(nums,0,nums.size()-1,k);
    }
};

1.4 最⼩k个数

  1. 题⽬链接:面试题 17.14. 最小K个数
  2. 题⽬描述:
    在这里插入图片描述
  3. 解法(快速选择算法):
    算法思路:
    在快排中,当我们把数组「分成三块」之后: [l, left] [left + 1, right - 1][right, r] ,我们可以通过计算每⼀个区间内元素的「个数」,进⽽推断出最⼩的 k 个数在哪些区间⾥⾯。那么我们可以直接去「相应的区间」继续划分数组即可。
  4. C++代码
class Solution {
public:
    void Sort(vector<int>& arr, int l, int r, int k) 
    {
        if (l >= r)return;
        int cur = l, left = l - 1, right = r + 1;
        // 1. 随机选择⼀个基准元素 + 数组分三块
        int ret = getRandom(arr, l, r);
        while (cur < right) 
        {
            if (arr[cur] < ret)swap(arr[++left], arr[cur++]);
            else if (arr[cur] == ret)cur++;
            else swap(arr[--right], arr[cur]);
        }
        // [l, left][left + 1, right - 1] [right, r]
		// 2. 分情况讨论
        int a = left - l + 1, b = right - left - 1;
        if (a > k) Sort(arr, l, left, k);
        else if (a + b >= k) return;
        else Sort(arr, right, r, k - a - b);
    }
    int getRandom(vector<int>& arr, int l, int r) 
    {
        return arr[rand() % (r - l + 1) + l];
    }
    vector<int> smallestK(vector<int>& arr, int k) 
    {
        srand(time(NULL));
        Sort(arr, 0, arr.size() - 1, k);
        return {arr.begin(), arr.begin() + k};
    }
};

二、分治- - -归并 示例:

2.1 排序数组

  1. 题⽬链接:912. 排序数组
  2. 题⽬描述:
    在这里插入图片描述
  3. 解法(归并排序):
    算法思路:
    归并排序的流程充分的体现了「分⽽治之」的思想,⼤体过程分为两步:
    ◦ 分:将数组⼀分为⼆为两部分,⼀直分解到数组的⻓度为 1 ,使整个数组的排序过程被分为「左半部分排序」 + 「右半部分排序」;
    ◦ 治:将两个较短的「有序数组合并成⼀个⻓的有序数组」,⼀直合并到最初的⻓度。
  4. C++代码
class Solution {
    vector<int> v;
public:
    vector<int> sortArray(vector<int>& nums) 
    {
        v.resize(nums.size());
        MergeSort(nums,0,nums.size()-1);
        return nums;
    }
    void MergeSort(vector<int>&nums,int left,int right)
    {
        //1.递归出口
        if(left>=right)return;
        //2.选择中间点划分区间---防溢出
        // [left, mid] [mid + 1, right]
        int mid=(right+left)>>1;
        //3.把左右区间排序
        MergeSort(nums,left,mid);
        MergeSort(nums,mid+1,right);
        //4.合并两个有序数组
        int cur1=left,cur2=mid+1,i=0;
        //vector<int> v(right-left+1);//相比于局部全局的更有效率
        while(cur1<=mid&&cur2<=right)
            v[i++]=nums[cur1]<nums[cur2]?nums[cur1++]:nums[cur2++];
        while(cur1<=mid)v[i++]=nums[cur1++];
        while(cur2<=right)v[i++]=nums[cur2++];
        //5.还原
        for(int i=left;i<=right;i++)nums[i]=v[i-left];
    }
};

2.2 交易逆序对的总数

  1. 题⽬链接:LCR 170. 交易逆序对的总数
  2. 题⽬描述:
    在这里插入图片描述
  3. 解法(利⽤归并排序的过程 — 分治):
    算法思路:
    ⽤归并排序求逆序数是很经典的⽅法,主要就是在归并排序的合并过程中统计出逆序对的数量,也就是在合并两个有序序列的过程中,能够快速求出逆序对的数量。我们将这个问题分解成⼏个⼩问题,逐⼀破解这道题。
    注意:默认都是升序,如果掌握升序的话,降序的归并过程也是可以解决问题的。
  • 先解决第⼀个问题,为什么可以利⽤归并排序?
    如果我们将数组从中间划分成两个部分,那么我们可以将逆序对产⽣的⽅式划分成三组:
    • 逆序对中两个元素:全部从左数组中选择
    • 逆序对中两个元素:全部从右数组中选择
    • 逆序对中两个元素:⼀个选左数组另⼀个选右数组
    根据排列组合的分类相加原理,三种种情况下产⽣的逆序对的总和,正好等于总的逆序对数量。⽽这个思路正好匹配归并排序的过程:
    • 先排序左数组;
    • 再排序右数组;
    • 左数组和右数组合⼆为⼀。
    因此,我们可以利⽤归并排序的过程,先求出左半数组中逆序对的数量,再求出右半数组中逆序对的数量,最后求出⼀个选择左边,另⼀个选择右边情况下逆序对的数量,三者相加即可。

  • 解决第⼆个问题,为什么要这么做?
    在归并排序合并的过程中,我们得到的是两个有序的数组。我们是可以利⽤数组的有序性,快速统计出逆序对的数量,⽽不是将所有情况都枚举出来。最核⼼的问题,如何在合并两个有序数组的过程中,统计出逆序对的数量?合并两个有序序列时求逆序对的⽅法有两种

<1> 快速统计出某个数前⾯有多少个数⽐它⼤;(升序)
通过⼀个⽰例来演⽰⽅法⼀:
假定已经有两个已经有序的序列以及辅助数组 left = [5, 7, 9] right = [4, 5, 8] help = [ ],通过合并两个有序数组的过程,来求得逆序对的数量:
规定如下定义来叙述过程: cur1 遍历 left 数组,cur2 遍历 right 数组 ret 记录逆序对的数量
• 第⼀轮循环:left[cur1] > right[cur2],由于两个数组都是升序的,那么我们可以断定,此刻 left 数组中[cur1, 2] 区间内的 3 个元素均可与 right[cur2] 的元素构成逆序对,因此可以累加逆序对的数量 ret += 3,并且将 right[cur2] 加⼊到辅助数组中,cur2++ 遍历下⼀个元素。 第⼀轮循环结束后:left = [5, 7, 9] right = [x, 5, 8] help = [4] ret = 3 cur1 = 0 cur2 = 1
• 第⼆轮循环:left[cur1] == right[cur2],因为 right[cur2] 可能与 left 数组中往后的元素构成逆序对,因此我们需要将 left[cur1] 加⼊到辅助数组中去,此时没有产⽣逆序对,不更新 ret。 第⼆轮循环结束后:left = [x, 7, 9] right = [x, 5, 8] help = [4, 5] ret = 3 cur1 = 1 cur2 = 1
• 第三轮循环:left[cur1] > right[cur2],与第⼀轮循环相同,此刻 left 数组中[cur1, 2] 区间内的 2 个元素均可与 right[cur2] 的元素构成逆序对,更新 ret 的值为 ret += 2,并且将 right[cur2] 加⼊到辅助数组中去,cur2++ 遍历下⼀个元素。 第三轮循环结束后:left = [x, 7, 9] right = [x, x, 8] help = [4, 5, 5] ret = 5 cur1 = 1 cur2 = 2
• 第四轮循环:left[cur1] < right[cur2],由于两个数组都是升序的,因此我们可以确定 left[cur1] ⽐ right 数组中的所有元素都要⼩。left[cur1] 这个元素是不可能与 right 数组中的元素构成逆序对。因此,⼤胆的将 left[cur1] 这个元素加⼊到辅助数组中去,不更细 ret 的值。 第四轮循环结束后:left = [x, x, 9] right = [x, x, 8] help = [4, 5, 5, 7] ret = 5 cur1 = 2 cur2 = 2
• 第五轮循环:left[cur1] > right[cur2],与第⼀、第三轮循环相同。此时 left 数组内的 1 个元素能与 right[cur2] 构成逆序对,更新 ret 的值,并且将 right[cur2] 加⼊到辅助数组中去。 第五轮循环结束后:left = [x, x, 9] right = [x, x, x] help = [4, 5, 5, 7, 8] ret = 6 cur1 = 2 cur2 = 2
• 处理剩余元素:如果是左边出现剩余,说明左边剩下的所有元素都是⽐右边元素⼤的,但是它们都是已经被计算过的(我们以右边的元素为基准的),因此不会产⽣逆序对,仅需归并排序即可;如果是右边出现剩余,说明右边剩下的元素都是⽐左边⼤的,不符合逆序对的定义,因此也不需要处理,仅需归并排序即可。
整个过程只需将两个数组遍历⼀遍即可,时间复杂度为 O(N)。
• 由上述过程我们可以得出⽅法⼀统计逆序对的关键点:
在合并有序数组的时候,遇到左数组当前元素 > 右数组当前元素时,我们可以通过计算左数组中剩余元素的⻓度,就可快速求出右数组当前元素前⾯有多少个数⽐它⼤,对⽐解法⼀中⼀个⼀个枚举逆序对效率快了许多。

<2> 快速统计出某个数后⾯有多少个数⽐它⼩;–>对于这个类型考虑用降序(下面的方式是升序)
依旧通过⼀个⽰例来演⽰⽅法⼆:
假定已经有两个已经有序的序列以及辅助数组 left = [5, 7, 9] right = [4, 5, 8] help = [ ],通过合并两个有序数组的过程,来求得逆序对的数量:规定如下定义来叙述过程: cur1 遍历 left 数组,cur2 遍历 right 数组 ret 记录逆序对的数量
• 第⼀轮循环:left[cur1] > right[cur2],先不要着急统计,因为我们要找的是当前元素后⾯有多少⽐它⼩的,这⾥虽然出现了⼀个,但是 right 数组中依旧还可能有其余⽐它⼩的。因此此时仅将 right[cur2] 加⼊到辅助数组中去,并且将 cur2++。 第⼀轮循环结束后:left = [5, 7, 9] right = [x, 5, 8] help = [4] ret = 0 cur1 = 0 cur2 = 1
• 第⼆轮循环:left[cur1] == right[cur2],由于两个数组都是升序,这个时候对于元素 left[cur1] 来说,我们已经可以断定 right 数组中 [0, cur2) 左闭右开区间上的元素都是⽐它⼩的。因此此时可以统计逆序对的数量 ret += cur2 - 0,并且将 left[cur1] 放⼊到辅助数组中去,cur1++ 遍历下⼀个元素。 第⼆轮循环结束后:left = [x, 7, 9] right = [x, 5, 8] help = [4, 5] ret = 1 cur1 = 1 cur2 = 1
• 第三轮循环:left[cur1] > right[cur2],与第⼀轮循环相同,直接将 right[cur2] 加⼊到辅助数组中去,cur2++ 遍历下⼀个元素。 第三轮循环结束后:left = [x, 7, 9] right = [x, x, 8] help = [4, 5, 5] ret = 1 cur1 = 1 cur2 = 2
• 第四轮循环:left[cur1] < right[cur2],由于两个数组都是升序的,这个时候对于元素 left[cur1] 来说,我们依旧已经可以断定 right 数组中 [0, cur2) 左闭右开区间上的元素都是⽐它⼩的。因此此时可以统计逆序对的数量 ret += cur2 - 0,并且将 left[cur1] 放⼊到辅助数组中去,cur1++ 遍历下⼀个元素。 第四轮循环结束后:left = [9] right = [8] help = [4, 5, 5, 7] ret = 3 cur1 = 2 cur2 = 2
• 第五轮循环:left[cur1] > right[cur2],与第⼀、第三轮循环相同。直接将 right[cur2] 加⼊到辅助数组中去,cur2++ 遍历下⼀个元素。 第五轮循环结束后:left = [x, x, 9] right = [x, x, x] help = [4, 5, 5, 7, 8] ret = 3 cur1 = 2 cur2 = 2
• 处理剩余元素:
<1> 如果是左边出现剩余,说明左边剩下的所有元素都是⽐右边元素⼤的,但是相⽐较于⽅法⼀,逆序对的数量是没有统计过的。因此,我们需要统计 ret 的值:
◦ 设左边数组剩余元素的个数为 leave
◦ ret += leave * (cur2 - 0)
对于本题来说,处理剩余元素的时候, left 数组剩余 1 个元素,cur2 - 0 = 3,因此 ret 需要类加上 3,结果为 6。与⽅法⼀求得的结果相同。
<2>如果是右边出现剩余,说明右边剩下的元素都是⽐左边⼤的,不符合逆序对的定义,因此也不需要处理,仅需归并排序即可。
整个过程只需将两个数组遍历⼀遍即可,时间复杂度依旧为 O(N)。
由上述过程我们可以得出⽅法⼆统计逆序对的关键点:
在合并有序数组的时候,遇到左数组当前元素 <= 右数组当前元素时,我们可以通过计算右数组已经遍历过的元素的⻓度,快速求出左数组当前元素后⾯有多少个数⽐它⼤。但是需要注意的是,在处理剩余元素的时候,⽅法⼆还需要统计逆序对的数量。

  1. C++代码
class Solution {
    vector<int> v;
public:
    int mergeSort(vector<int>& rd,int left,int right)
    {
        if(left>=right)return 0;
        //1.找中间点将数组分成两部分
        int mid=left+((right-left)>>1);
        //2.左边个数+排序+右边个数+排序
        int ret=0;
        ret+=mergeSort(rd,left,mid);
        ret+=mergeSort(rd,mid+1,right);
        //3.一左一右的个数
        int cur1=left,cur2=mid+1,i=0;
        while(cur1<=mid&&cur2<=right)
        {
            // if(rd[cur1]<=rd[cur2])//找cur2前面有多少个大于它的cur1--->升序
            // {
            //     v[i++]=rd[cur1++];
            // }
            // else
            // {
            //     ret+=mid-cur1+1;
            //     v[i++]=rd[cur2++];
            // }

            if(rd[cur1]<=rd[cur2])//找cur1后面有多少个比它小的cur2--->降序
            {
                v[i++]=rd[cur2++];
            }
            else
            {
                ret+=right-cur2+1;
                v[i++]=rd[cur1++];
            }
        }
        //4.处理一下排序
        while(cur1<=mid)v[i++]=rd[cur1++];
        while(cur2<=right)v[i++]=rd[cur2++];
        //5.还原
        for(int i=left;i<=right;i++)
        {
            rd[i]=v[i-left];
        }
        return ret;
    }
    int reversePairs(vector<int>& record) 
    {
        v.resize(record.size());
        return mergeSort(record,0,record.size()-1);
    }
};

2.3 计算右侧⼩于当前元素的个数

  1. 题⽬链接:315. 计算右侧⼩于当前元素的个数
  2. 题⽬描述:
    在这里插入图片描述
  3. 解法(归并排序):
    算法思路:
    这⼀道题的解法与 求数组中的逆序对 的解法是类似的,但是这⼀道题要求的不是求总的个数,⽽是要返回⼀个数组,记录每⼀个元素的右边有多少个元素⽐⾃⼰⼩。但是在我们归并排序的过程中,元素的下标是会跟着变化的,因此我们需要⼀个辅助数组,来将数组元素和对应的下标绑定在⼀起归并,也就是再归并元素的时候,顺势将下标也转移到对应的位置上。
    算法流程:
    • 创建两个全局的数组:
    vector index:记录下标
    vector ret:记录结果
    index ⽤来与原数组中对应位置的元素绑定,ret ⽤来记录每个位置统计出来的逆序对的个数。
    • countSmaller() 主函数:
    a. 计算 nums 数组的⼤⼩为 n;
    b. 初始化定义的两个全局的数组;
    <1> 为两个数组开辟⼤⼩为 n 的空间
    <2> index 初始化为数组下标;
    <3> ret 初始化为 0
    c. 调⽤ mergeSort() 函数,并且返回 ret 结果数组。
    • void mergeSort( vector& nums, int left, int right ) 函数:
    函数设计:通过修改全局的数组 ret, 统计出每⼀个位置对应的逆序对的数量,并且排序;
    ⽆需返回值,因为直接对全局变量修改,当函数结束的时候,全局变量已经被修改成最后的结果。
    • mergeSort() 函数流程:
    a. 定义递归出⼝:left >= right 时,直接返回;
    b. 划分区间:根据中点 mid,将区间划分为 [left, mid] 和 [mid + 1, right];
    c. 统计左右两个区间逆序对的数量:
    <1> 统计左边区间 [left, mid] 中每个元素对应的逆序对的数量到 ret 数组中,并排序;
    <2> 统计右边区间 [mid + 1, right] 中每个元素对应的逆序对的数量到 ret 数组中,并排序。
    d. 合并左右两个有序区间,并且统计出逆序对的数量:
    <1> 创建两个⼤⼩为 right - left + 1 ⼤⼩的辅助数组:
    • numsTmp: 排序⽤的辅助数组;
    • indexTmp:处理下标⽤的辅助数组。
    <2> 初始化遍历数组的指针:cur1 = left(遍历左半部分数组)cur2 = mid + 1(遍历右半边数
    组)dest = 0(遍历辅助数组)curRet(记录合并时产⽣的逆序对的数量);
    <3>循环合并区间:
    • 当 nums[cur1] <= nums[cur2] 时:
    ◦ 说明此时 [mid + 1, cur2) 之间的元素都是⼩于 nums[cur1] 的,需要累加到 ret 数组的 indext[cur1] 位置上(因为 index 存储的是元素对应位置在原数组中的下标)
    归并排序:不仅要将数据放在对应的位置上,也要将数据对应的坐标也放在对应的位置上,使数据与原始的下标绑定在⼀起移动。
    • 当 nums[cur1] > nums[cur2] 时,⽆需统计,直接归并,注意 index 也要跟着归并。
    <4> 处理归并排序中剩余的元素;
    • 当左边有剩余的时候,还需要统计逆序对的数量;
    • 当右边还有剩余的时候,⽆需统计,直接归并。
    <5> 将辅助数组的内容替换到原数组中去;
  4. C++代码
class Solution {
    vector<int> ret;//存储结果
    vector<int> index;//记录 nums 中当前元素的原始下标
    int tmpindex[100010];
    int tmpnums[100010];
public:
    void mergeSort(vector<int>&nums,int left,int right)
    {
        if(left>=right)return;
        int mid=left+((right-left)>>1);
        mergeSort(nums,left,mid);
        mergeSort(nums,mid+1,right);
        int cur1=left,cur2=mid+1,i=0;//i每次从0开始
        while(cur1<=mid&&cur2<=right)
        {
            if(nums[cur1]<=nums[cur2])
            {
                tmpnums[i]=nums[cur2];
                tmpindex[i]=index[cur2];
                i++;
                cur2++;
            }
            else
            {
                ret[index[cur1]] += right - cur2 + 1;
                tmpnums[i]=nums[cur1];
                tmpindex[i]=index[cur1];
                i++;
                cur1++;
            }
        }
        while(cur1<=mid)
        {
            tmpnums[i]=nums[cur1];
            tmpindex[i]=index[cur1];
            i++;
            cur1++;
        }
        while(cur2<=right)
        {
            tmpnums[i]=nums[cur2];
            tmpindex[i]=index[cur2];
            i++;
            cur2++;
        }
        for(int i=left;i<=right;i++)
        {
            nums[i]=tmpnums[i-left];
            index[i]=tmpindex[i-left];
        }
    }
    vector<int> countSmaller(vector<int>& nums) 
    {
        ret.resize(nums.size());
        index.resize(nums.size());
        // 初始化⼀下 index 数组
        for(int i=0;i<nums.size();i++) index[i]=i;
        mergeSort(nums,0,nums.size()-1);
        return ret;
    }
};

2.4 翻转对

  1. 题⽬链接:493. 翻转对
  2. 题⽬描述:
    在这里插入图片描述
  3. 解法(归并排序):
    算法思路:
    ⼤思路与求逆序对的思路⼀样,就是利⽤归并排序的思想,将求整个数组的翻转对的数量,转换成三部分:左半区间翻转对的数量,右半区间翻转对的数量,⼀左⼀右选择时翻转对的数量。重点就是在合并区间过程中,如何计算出翻转对的数量。
    与上个问题不同的是,上⼀道题我们可以⼀边合并⼀遍计算,但是这道题要求的是左边元素⼤于右边元素的两倍,如果我们直接合并的话,是⽆法快速计算出翻转对的数量的。
    例如 left = [4, 5, 6] right = [3, 4, 5] 时,如果是归并排序的话,我们需要计算 left 数组中有多少个能与 3 组成翻转对。但是我们要遍历到最后⼀个元素 6 才能确定,时间复杂度较⾼。 因此我们需要在归并排序之前完成翻转对的统计。
    下⾯依旧以⼀个⽰例来模仿两个有序序列如何快速求出翻转对的过程:
    假定已经有两个已经有序的序列 left = [4, 5, 6] right = [1, 2, 3] 。
    ⽤两个指针 cur1 cur2 遍历两个数组。
    ◦ 对于任意给定的 left[cur1] ⽽⾔,我们不断地向右移动 cur2,直到 left[cur1] <= 2 * right[cur2]。此时对于 right 数组⽽⾔,cur2 之前的元素全部都可以与 left[cur1] 构成翻转对。
    ◦ 随后,我们再将 cur1 向右移动⼀个单位,此时 cur2 指针并不需要回退(因为 left 数组是升序的)依旧往右移动直到 left[cur1] <= 2 * right[cur2]。不断重复这样的过程,就能够求出所有左右端点分别位于两个⼦数组的翻转对数⽬。由于两个指针最后都是不回退的的扫描到数组的结尾,因此两个有序序列求出翻转对的时间复杂度是 O(N)。
    综上所述,我们可以利⽤归并排序的过程,将求⼀个数组的翻转对转换成求 左数组的翻转对数量 + 右数组中翻转对的数量 + 左右数组合并时翻转对的数量。
  4. C++代码
class Solution {
    int tmp[50010];
public:
    int mergeSort(vector<int>&nums,int left,int right)
    {
        if(left>=right)return 0;
        int mid=left+((right-left)>>1);
        int ret=0;
        ret+=mergeSort(nums,left,mid);
        ret+=mergeSort(nums,mid+1,right);
        int cur1=left,cur2=mid+1,i=left;
        while(cur1<=mid)//降序
        {
            while(cur2<=right&&nums[cur1]/2.0<=nums[cur2])cur2++;
            if(cur2>right)break;
            ret+=right-cur2+1;
            cur1++;
        }
        cur1=left;cur2=mid+1;
        while(cur1<=mid&&cur2<=right)
            tmp[i++]=nums[cur1]<=nums[cur2]?nums[cur2++]:nums[cur1++];
        while(cur1<=mid)tmp[i++]=nums[cur1++];
        while(cur2<=right)tmp[i++]=nums[cur2++];
        for(int i=left;i<=right;i++)nums[i]=tmp[i];
        return ret;
    }
    int reversePairs(vector<int>& nums) 
    {
        return mergeSort(nums,0,nums.size()-1);
    }
};
-----------------------------------------------------------------------------------------------

class Solution {
    int tmp[50010];
public:
    int mergeSort(vector<int>&nums,int left,int right)
    {
        if(left>=right)return 0;
        int mid=left+((right-left)>>1);
        int ret=0;
        ret+=mergeSort(nums,left,mid);
        ret+=mergeSort(nums,mid+1,right);
        int cur1=left,cur2=mid+1,i=left;
        while(cur2<=right)//升序
        {
            while(cur1<=mid&&nums[cur1]/2.0<=nums[cur2])cur1++;
            if(cur1>mid)break;
            ret+=mid-cur1+1;
            cur2++;
        }
        cur1=left;cur2=mid+1;
        while(cur1<=mid&&cur2<=right)
            tmp[i++]=nums[cur1]>=nums[cur2]?nums[cur2++]:nums[cur1++];
        while(cur1<=mid)tmp[i++]=nums[cur1++];
        while(cur2<=right)tmp[i++]=nums[cur2++];
        for(int i=left;i<=right;i++)nums[i]=tmp[i];
        return ret;
    }
    int reversePairs(vector<int>& nums) 
    {
        return mergeSort(nums,0,nums.size()-1);
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小沈YO.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值