分治算法之归并排序

发布于:2025-06-19 ⋅ 阅读:(11) ⋅ 点赞:(0)

在这里插入图片描述

每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry”

绪论​:
本章将学习分治算法,将通过练习促学习的方式来学习,主要通过先从归并算法开始带你打好基础,后续三道困难题都将在这个排序的基础上进行扩展,利用归并的思想来解决,在排序的过程中快速查找在自己后面的比自己小的值(降序排序),以及在自己前面比自己大的值(升序排序),具体细节让我们见文章吧,。
————————
早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。


分治(归并排序)

具体训练:

1. 归并排序

题目:

在这里插入图片描述
归并排序的核心思想:

  1. 根据中间的mid分成两部分:
  2. 先排左边:内部有是一个归并,当数组只有一个值时返回
  3. 再拍右边:内部同样是一个归并,当数组只有一个值时返回
  4. 将左右两个数组合并成一个有序数组返回
class Solution {
public:

    vector<int> tmp;

    vector<int> sortArray(vector<int>& nums) {
        msort(nums,0,nums.size()-1);
        return nums;
    }
    //Merge Sort
    void msort(vector<int>& nums,int left,int right){
        if(left >= right) return ;
        int mid = left + (right - left) / 2;
    //mid 1 2 3 4 mid = 1
    // 2 3 mid 
        msort(nums,left,mid);
        msort(nums,mid+1,right);
 
        //left ~ mid - 1
        //mid ~ right
        tmp.clear();
        int cur1 = left,cur2 = mid + 1;
        while(cur1 <= mid  && cur2 <= right){
            if(nums[cur1] < nums[cur2]) tmp.push_back(nums[cur1++]);
            else tmp.push_back(nums[cur2++]);
        }
        //处理剩下的
        while(cur1 <=  mid){
            tmp.push_back(nums[cur1++]);
        //处理剩下的
        }
        while(cur2 <= right){
            tmp.push_back(nums[cur2++]);
        }

        for(int i = left;i <= right;i++)
            nums[i] = tmp[i - left];
    }
};

其中本质就是后序遍历的方法:先遍历左,左边结束后才到右,最后到结点:
在这里插入图片描述

快排是每一层选择一个基准元素key,然后进行分组后重复,直到数组分成一个元素时分组结束
在这里插入图片描述
而快排就相当于一种前序遍历,处理完结点后分成两组后再分别处理:
本质区别就是处理数组时机不同

2. 交易逆序对的总数

题目:

在这里插入图片描述

分析题目

对于暴力解法来说:比较简单两层暴力枚举即可找到最多的逆序对(但一定是会超时的)
优化方法:
在这里插入图片描述
上图描述:

  1. 首先将一整个区间划分成两部分 a、b
  2. 然后在新区域a、b中找逆序对,得到a、b个
  3. 然后再将 a、b区域联合起来看,选择一个a区域中的逆序对,然后再选择一个b区域的,最终组合成新的逆序对,个数为c
  4. 那么最终逆序对个数为 a + b + c

顺序是:先挑a区域、再挑b区域、最后查询 a 和 b 组合(左半部分、右半部分、一左一右)
那么再次优化,先找左半部分,找完后进行排序、同理找完右半部分,进行排序,最后一左一右(因为排序后找一左一右就有单调性了就能快速的确定个数!)
最终:本质就很归并排序是一样的了!!(如下图)
在这里插入图片描述
其中对合并排序和其中需要的其他操作来找结果的详细解释(如下图):
在这里插入图片描述

当到达归并排序的某一层时的操作:

  1. 首要目的是为了合并,但同时也要记录逆序对的个数!
  2. 回顾个数的计算:a(左) + b(右) + c(左右),其中a、b在上一层已经计算了,所以本层只用计算 c 即可
  3. 记录个数(c 一左一右):那么也就是要找 cur1 后面能接的 cur2(记住)
  4. 回到公式当 num[cur1] <= num[cur2] 时就正常进行归并排序,cur1++
  5. 而当 num[cur1] > num[cur2] 时,这时cur1即往后的值都是大于cur2的(因为a区间是始终升序的),所以个数就能直接增加 ret += mid - cur1 + 1个,然后进行正常归并排序步骤cur2++

题目核心逻辑

class Solution {
public:
    
    int reversePairs(vector<int>& record) {
        return mergesort(record,0,record.size()-1);
    }

    //归并
    int mergesort(vector<int>& record,int left,int right)
    {
        if(left >= right) return 0;

        int mid = left + (right - left) / 2;
        
        int a = mergesort(record,left,mid);
        int b = mergesort(record,mid+1,right);

        vector<int> temp;
        // c
        int c= 0;
        //进行归并,此处是升序,取出小的放入容器中
        int cur1 = left,cur2 = mid+1;
        while(cur1 <= mid & cur2 <= right)
        {
            //策略1:找前面有多少大的,针对cur2来看
            if(record[cur1] > record[cur2]){
                c += mid - cur1 + 1;
                temp.push_back(record[cur2]);
                cur2++;
            }
            else{
                //cur1 <= cur2
                temp.push_back(record[cur1]);
                cur1++;
            }
        }
        while(cur1 <= mid) temp.push_back(record[cur1++]);
        while(cur2 <= right) temp.push_back(record[cur2++]);

        for(int j = left; j <= right; j++)
            record[j] = temp[j - left];
        return a + b + c;
    }


};

3. 计算右侧小于当前元素的个数

题目:

在这里插入图片描述

分析题目

分析题目和例1:

  • 找当前这个数的右边有多少比自己小的
    在这里插入图片描述
    所以就和上题比较像,可以使用
    上一题中的第二个策略:找当前元素后边有多少个比我小前提降序

  • 在降序的前提下(这里的降序,本质就是在归并排序中执行的排序操作)

  • 排序后就是内部的一些问题,同样的也是 左区间 + 右区间 + 左右区间

  • 其中具体已经在上题中说明了,本题主要还是讲解左右区间:

  • 通过比较cur1、cur2找到比自己小的元素,具体公式如下图

  • 本题需要返回的是一个数组nums,其中它对应的是,它上方数值右边比他小的个数

  • 当cur1 > cur2时,因为是降序cur2右边都将比他小,所以就直接加上right - cur2 - 1
    在这里插入图片描述

如何处理当排序后其索引改变导致找不到原数据的问题

  • 那么其中就会有一个问题,当排完序后如何仍然找到对应的原始的下标呢(因为排完序后位置就发生了改变,低下记录值的数组nums就将不再对应上,所以就需要进行一定的处理)
  • 一般来说可以使用hash的kv进行绑定值和下标,但本题可能会出现重复的值所以就不适合使用
  • 那么可以使用一个index新数组进行绑定管理索引,其中在操作nums的同时index也是同样的改变
  • 这样index数组中的索引就始终对应了
    在这里插入图片描述

题目核心逻辑

本题重点:

  • 通过新增一个start_index索引数组来记录当前索引的位置
  • 并且在更改位置的过程中记录当前索引的位置,然后实时的也进行修改
  • 其中也多借助了一个index辅助数组进行存储移动前的索引的位置
class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        int n = nums.size();
        //归并排序
        vector<int> res(n);
        vector<int> start_index(n);
        for(int i = 0;i < nums.size();i++)
            start_index[i] = i;

        mergeSort(nums,res,start_index,0,n - 1);

        return res;
    }

    int temp[100010] = {};
    int index[100010] = {};

    void mergeSort(vector<int>& nums,vector<int>& res,vector<int>& start_index,int left,int right)
    {
        if(left >= right) return ;//当超过范围就返回

        int mid = (right + left) / 2;//取中进行划分

        mergeSort(nums,res,start_index,left,mid);
        mergeSort(nums,res,start_index,mid+1,right);
        //a + b,合并的同时进行计算
        int p1 = left,p2 = mid + 1,ti = 0;
        while( p1 <= mid && p2 <= right)
        {
            //降序,等于 在归并排序中本质放到哪里都行,但在本题需要找的是 p1 > p2 的才个数,所以将 等于 放到 < 处
            if(nums[p1] <= nums[p2]){
                index[ti] = start_index[p2];//存储他的下标
                temp[ti++] = nums[p2++]; 
            }
            else{
                //记录长度
                //start_index是他的原始索引位置
                res[start_index[p1]] += right - p2 + 1;
                index[ti] = start_index[p1];//存储他的下标
                temp[ti++] = nums[p1++];
            }
        }

        while(p1 <= mid) {
            res[start_index[p1]] += right - p2 + 1;
            index[ti] = start_index[p1];//存储他的下标
            temp[ti++] = nums[p1++];
        }
        while(p2 <= right) {
            index[ti] = start_index[p2];//存储他的下标
            temp[ti++] = nums[p2++]; 
        }

        //将temp放入数组
        for(int i = left;i <= right ;i++)
        {
            start_index[i] = index[i - left];//让原数位置也跟着移动
            nums[i] = temp[i - left];
            //移动值的同时移动index
            //num[i]是新值,i是新
        }
    }

};

在这里插入图片描述

4. 翻转对

题目:

在这里插入图片描述

分析题目

  • 分析题目:找出所有值是后面值的两倍的的个数
  • 如下图:3 > 1*2 共有两次所以也就是2
    在这里插入图片描述
    那么类似的还是找比自己小的元素,只不过此时是小两倍的元素,就会导致运算情况和归并并不一样了
    此时就需要将算左 右区间的情况单独出来看,用一个双指针的方式(这里就不过诉了看下图)来提前处理排好序的值,找到左 右区间合起来看时有多少符合翻转对的值,具体为什么不能放到归并中操作下述代码中已经注释
  • 一个基础的单调性双指针算法(有问题可以评论)
    在这里插入图片描述

题目核心逻辑

  • 具体细节写在注释
class Solution {
public:
    int reversePairs(vector<int>& nums) {
        return mergeSort(nums,0,nums.size()-1);
        
    }

    int tmpNums[50010];

    int mergeSort(vector<int>& nums, int left, int right) {
        if (left >= right)
            return 0;

        int mid = (left + right) / 2;
        int ret = 0;
        // [left, mid] [mid + 1, right]
        // 2. 处理左右两个区间
        ret += mergeSort(nums, left, mid);
        ret += mergeSort(nums, mid + 1, right);
        // 3. 处理⼀左⼀右的情况
        int cur1 = left, cur2 = mid + 1, i = 0;
        
        //双指针提前处理,计算左右区间中的值
         while (cur1 <= mid && cur2 <= right) // 降序排序
        {
            //主要不同就是在这里,不能再归并中同时处理就是因为,可能会跳过处理某些情况
            //若:当cur1 > cur2 时会不断的走cur1的而不会动 cur2,这样就会导致 cur1 > 2*cur2 其中的cur2不变导致 ret 无法遍历所有情况
            if (nums[cur1] >(long long) 2*nums[cur2]) {
                cur1++;
                ret += right - cur2 + 1;
                
            } else {
               cur2++;
            }
        }

        cur1 = left, cur2 = mid + 1;//恢复值
        //不放在一起的原因是,他们运算的过程不一致,主要体现在 nums[cur1] > 2*num[cur2] 时才需要记录答案,这里有许多细节
        //并不好叙述,主要就是理解他们 运算是不一样的 会导致某些问题的方式从而让 cur2 和 cur1 的比较并不正常(具体如上描述)
        while (cur1 <= mid && cur2 <= right) // 降序排序
        {
            if (nums[cur1] <= nums[cur2]) {
                tmpNums[i++] = nums[cur2++];
                
            } else {
                tmpNums[i++] = nums[cur1++];
            }
        }
        // 4. 处理剩余的排序⼯作
        while (cur1 <= mid) {
            tmpNums[i++] = nums[cur1++];
        }
        while (cur2 <= right) {
            tmpNums[i++] = nums[cur2++];
        }

        for (int j = left; j <= right; j++) {
            nums[j] = tmpNums[j - left];
        }
        
        return ret;
    }
};

网站公告

今日签到

点亮在社区的每一天
去签到