【优选算法】---分治 归并排序

分治 归并排序

一、排序数组 / 归并排序的复习

归并排序的主要步骤:

①:就是找一个数组里面的中间数mid,将这个数组分为两部分,左半部分和右半部分。然后再从左半部分和右半部分里面再找一个中间数mid,依次不断的递归下去,直到分到最小单位为止,排完序之后从下往上返回,这就是归并排序的递归。
②:合并两个有序数组(双指针法)
分别对左半部分数组和右半部分数组,定义一个下标移动的变量cur1和cur2,然后进行循环比较。

1、题目解析

题目链接:排序数组

2、算法原理

分治的思想:归并排序类似于二叉树的后序遍历

主要步骤:

1. 中间点的划分
2. 对左右区间的递归
3. 合并两个有序数组
4. 还原

3、代码

cpp 复制代码
class Solution 
{
    vector<int> tmp;
    // (2)第二种方法,在全局开一个临时数组tmp(用来保存每次递归合并的两个有序数组),
    //  然后就不用每次递归还创建tmp
public:
    vector<int> sortArray(vector<int>& nums) 
    {
       tmp.resize(nums.size());// 开空间
       mergeSort(nums,0,nums.size()-1);
       return nums;
    }

    void mergeSort(vector<int>& nums,int left,int right)
    {
        // 处理边界情况
        if(left>=right) return;

        // 1、中间点划分区间
        int mid=(left+right)>>1;
        // [left,mid]  [mid+1,right]

        // 2、归并的递归排序
        mergeSort(nums,left,mid);
        mergeSort(nums,mid+1,right);

        // 3、合并两个有序数组
        int cur1=left,cur2=mid+1,i=0;

        
        while(cur1<=mid&&cur2<=right)
            tmp[i++]=nums[cur1]<=nums[cur2]?nums[cur1++]:nums[cur2++];
            // 排的是升序

        // 处理没有遍历完的数组
        while(cur1<=mid) tmp[i++]=nums[cur1++];
        while(cur2<=right) tmp[i++]=nums[cur2++];


        // 4、还原
        for(int i=left;i<=right;i++)
        {
            nums[i]=tmp[i-left];
        }
    }

};

二、逆序对的总数

1、题目解析


数组中的逆序对 链接

2、算法原理

(1)将整个数组分为左半部分和右半部分,

(2)分别在左半部分找逆序对的个数,排完序。

(3)然后再在右半部分找逆序对的个数再排排序,

(4)最后再一左一右利用双指针的算法,固定一个然后移动另外一个在:一左一右->这两个数组里面分别找逆序对的个数,最后加起来就是整个数组的逆序对个数。

这样的解法的时间复杂度:N*logN


关键的细节:

3、代码

cpp 复制代码
class Solution 
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& nums) 
    {
       return mergeSort(nums,0,nums.size()-1);
    }

    int mergeSort(vector<int>& nums,int left,int right)
    {
        // 处理边界情况
        if(left>=right) return 0;
        
        // 1、找中间值mid划分为左右两部分
        int mid=(left+right)>>1;
        // [left,mid]  [mid+1,right]

        int ret=0;// 记录逆序对个数
        // 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)
        {
            if(nums[cur1]<=nums[cur2]) 
            {
                tmp[i++]=nums[cur1++];
            }
            else
            {
                //ret+=cur2-(mid+1)+1;  这种情况不行,因为我们是以左边为基准的,只要cur1遍历完,一左一右这个过程就结束了!

                ret+=mid-cur1+1;// 因为是升序,所以在左半部分,只要此时的cur1>cur2,那么位于cur1右侧~mid之间的数据全部>cur2
                tmp[i++]=nums[cur2++];
            }
        }

        // 3、处理没遍历完的
        while(cur1<=mid) tmp[i++]=nums[cur1++];
        while(cur2<=right) tmp[i++]=nums[cur2++];

        // 4、还原
        for(int j=left;j<=right;j++)
        {
            nums[j]=tmp[j-left];// 始终不太理解 还原的时候,为啥要用j-left???不能直接nums[j]=tmp[j]吗???
        }
        return ret;
    }
};

三、计算右侧小于当前元素的个数

1、题目解析

链接:计算右侧小于当前元素的个数

2、算法原理

这道题仍然是利用归并排序的分治思想

但是这次我们要排的是:降序!

但是有一点需要特别注意,这是一个重点也是一个难点。

题目中需要返回的是一个数组!
一个什么样的数组呢?原始数组下标所对应位置,有多少个右面比我小的元素。

这里的重点和难点就在于:我们如何找到我们正在遍历的数组(是打乱排完序的)中当前元素的原始下标!

解决办法就是:在进行分治归并排序之前,对原始数组利用哈希思想进行下标绑定。


就是你进行递归排序的时候,你的下标跟着你的数字是一起移动的!!!

3、代码

cpp 复制代码
class Solution 
{
    vector<int> ret;// 返回答案的数组
    vector<int> index;// 创建index数组的原因是:对排序前的 原始数组 进行下标"绑定"!
    int tmpNums[500010];// 这两个都是临时数组
    int tmpIndex[500010];
public:
    vector<int> countSmaller(vector<int>& nums) 
    {
        int n=nums.size();

        ret.resize(n);
        index.resize(n);

        // 排序前的 原始数组 进行下标"绑定"!
        for(int i=0;i<n;i++)
        {
            index[i]=i;
        }

        mergeSort(nums,0,n-1);
        return ret;
    }

    void mergeSort(vector<int>& nums,int left,int right)
    {

        // 处理边界情况
        if(left>=right) return ;

        // 1、找中间数mid
        int mid=(left+right)>>1;
        //[left,mid]  [mid+1,right]

        //2、归并排序
        mergeSort(nums,left,mid);
        mergeSort(nums,mid+1,right);

        // 3、找
        int cur1=left,cur2=mid+1,i=0;
        while(cur1<=mid&&cur2<=right)
        {
            // 降序排序
            if(nums[cur1]<=nums[cur2])
            {
                tmpNums[i]=nums[cur2];
                tmpIndex[i++]=index[cur2++];
            }
            else
            {
                //tmpNums[index[i]]+=right-cur2+1;

                // 重点!(统计此时cur1位置有多少符合条件的,因为cur1原本是在nums中遍历的,但是由于,index跟nums是一一对应的关系,所以index[cur1]就代表着原始数据的下标)
                ret[index[cur1]]+=right-cur2+1;

                tmpNums[i]=nums[cur1];
                tmpIndex[i++]=index[cur1++];
            }
        }

        // 处理没遍历完的
        while(cur1<=mid)
        {
            tmpNums[i]=nums[cur1];
            tmpIndex[i++]=index[cur1++];// 绑定的下标数组 也要跟着还原。++只需要进行一次即可
        }
        while(cur2<=right)
        {
            tmpNums[i]=nums[cur2];
            tmpIndex[i++]=index[cur2++];
        }

        // 4、还原
        for(int j=left;j<=right;j++)
        {
            nums[j]=tmpNums[j-left];
            index[j]=tmpIndex[j-left];
        }

    }
};

四、翻转对

1、题目解析

链接:翻转对

2、算法原理

主要思想还是用归并排序的分治思想

1、运用递归分别:处理左半部分、右半部分

2、运用" 单调性+同向的双指针 " 处理一左一右的情况

3、代码

cpp 复制代码
class Solution 
{
    int tmp[50010];
public:
    int reversePairs(vector<int>& nums) 
    { 
        return mergeSort(nums,0,nums.size()-1);
    }

    int mergeSort(vector<int>& nums,int left,int right)
    {
        // 处理边界情况
        if(left>=right) return 0;

        int ret=0;

        // 1、中间数mid
        int mid=(left+right)>>1;

        // 2、处理左半部分、右半部分
        ret+=mergeSort(nums,left,mid);
        ret+=mergeSort(nums,mid+1,right);
        
        // 3、一左一右(固定cur1不动,cur2移动)
        int cur1=left,cur2=mid+1,i=left;
        while(cur1<=mid)
        {
            while(cur2<=right&&nums[cur1]/2.0<=nums[cur2])// 因为 *2会溢出,所以我们用/2.0
            {
                cur2++;
            }
            // 接下来就是符合条件的
            ret+=right-cur2+1;
            cur1++;
        }

        // 4、合并两个有序数组
        cur1=left,cur2=mid+1;
        
        while(cur1<=mid&&cur2<=right)
        {
            tmp[i++]=nums[cur1]>=nums[cur2]?nums[cur1++]:nums[cur2++];//降序排序,把大的放前面
        }
        // 处理 未遍历完的
        while(cur1<=mid) tmp[i++]=nums[cur1++];
        while(cur2<=right)  tmp[i++]=nums[cur2++];

        // 4、还原
        for(int j=left;j<=right;j++)
        {
            nums[j]=tmp[j];
        }
        return ret;
    }
};
相关推荐
A懿轩A36 分钟前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
古希腊掌管学习的神37 分钟前
[搜广推]王树森推荐系统——矩阵补充&最近邻查找
python·算法·机器学习·矩阵
云边有个稻草人40 分钟前
【优选算法】—复写零(双指针算法)
笔记·算法·双指针算法
机器视觉知识推荐、就业指导41 分钟前
C++设计模式:享元模式 (附文字处理系统中的字符对象案例)
c++
半盏茶香41 分钟前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
忘梓.2 小时前
解锁动态规划的奥秘:从零到精通的创新思维解析(3)
算法·动态规划
Ronin3052 小时前
11.vector的介绍及模拟实现
开发语言·c++
✿ ༺ ོIT技术༻2 小时前
C++11:新特性&右值引用&移动语义
linux·数据结构·c++
字节高级特工2 小时前
【C++】深入剖析默认成员函数3:拷贝构造函数
c语言·c++
tinker在coding4 小时前
Coding Caprice - Linked-List 1
算法·leetcode