分治—快排与归并算法的学习与习题

目录

1.快速排序

1.1颜色分类

1.2排序数组

1.3数组中第K个最大元素

1.4库存管理III

2.归并排序

2.1排序数组

2.2交易逆序对的总数

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

2.4翻转对


1.快速排序

这里使用的快速排序是三路快排

先简单介绍一下快速排序的思路,对于每次排序,选定一个基准值,根据这个基准值对当前区间的元素进行排序,假设使用升序的方法

那么基准值左侧全部小于key,基准值右侧全部大于key,中间的部分是值均为key的一段数组,那么此时数组整体来看是有序的,左侧小右侧大,只不过左右区间内部还不是有序的,所以要进一步递归进入左右数组,继续使用快排

这就是三路快排的思路,因为加入了一个等于key的区间,这使得如果数组内部有大量重复数据时的时间复杂度大大减小,假设一个数组全部都是一个值,那么就会得到最大的遍历次数,对于普通快排,每次遍历的元素个数递减:n+(n-1)+(n-2)+..+2+1,得到O(n^2)的时间复杂度

但是加上了中间这个区间,只需要遍历n次即可将全部元素相同的这个数组排好

1.1颜色分类

75. 颜色分类 - 力扣(LeetCode)

对于这样的数组,其实就是选定基准值为1,将数组进行三路快排

这里要用到三个指针,一个指针控制左侧的小区间,一个指针控制右侧的大区间,一个指针用于遍历数组,因为左侧小右侧大,所以我们将左指针left设为-1,右指针right设为num.size(),在数组边界待命,然后从0开始遍历数组,使用i遍历

如果碰到的元素小于key,那么把元素丢到左侧,并让i++,注意把元素丢到左侧前要先left++

如果等于key,就让i++即可

如果大于key,那么将元素丢到右侧,但是注意,我们从左向右扫描整个数组,i与left之间的元素都应该是key,但是对于右侧未扫描的元素,我们无法确定交换过来的元素和key的关系,所以i不能移动,要进行下一轮的判断

代码部分

class Solution {
public:
    void sortColors(vector<int>& nums) {
        int left=-1;
        int right=nums.size();
        int i=left+1;
        while(i<right){
            if(nums[i]<1)swap(nums[++left],nums[i++]);//左侧已经扫描过,交换的元素不用再次检查
            else if(nums[i]==1)nums[i++];
            else swap(nums[--right],nums[i]);//右侧没扫描,i不动
        }
        return;
    }
};

1.2排序数组

912. 排序数组 - 力扣(LeetCode)

这道题就是上一题的进阶,上一次由于只有三种数据,选定中间值作为基准值,遍历一边即可完成排序,而对于普通的数组进行排序,每次都要选定一个随机元素作为基准值,因为随机出的基准值可以让排序效率稍微高一点

这里就需要使用到rand函数和srand函数,主函数使用时间戳的数据作为种子,让rand函数可以真正随机出数字,让rand的结果%上区间的长度再加上左端点的下标,即可得到当前区间的随机基准值,其他部分没有难点,只不过就是对左右数组进一步递归快排

代码部分

将代码拆分成几个函数部分,getRandom用于取出每个区间的基准值,将快排的实现操作放在myqsort中,注意最后划分的区间是[l,left][left+1,right-1][right,r],这是下一次排序的区间划分,注意如果传入的左右端点重合了,说明区间只有一个元素,直接返回即可

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        srand(time(NULL));
        int left = -1;
        int right = nums.size();
        int i = left + 1;
        myqsort(nums, left + 1, right - 1);
        return nums;
    }
    int getRandom(vector<int>& nums, int l, int r) {
        int pos = rand() % (r - l + 1) + l;
        return nums[pos];
    }
    void myqsort(vector<int>& nums, int l, int r) {
        if(l>=r)return;
        int k = getRandom(nums, l, r);
        int left = l - 1;
        int right = r + 1;
        int i = left + 1;
        while (i < right) {
            if (nums[i] < k)
                swap(nums[++left], nums[i++]);
            else if (nums[i] == k)
                nums[i++];
            else
                swap(nums[--right], nums[i]);
        }
        //[l,left][left+1,right-1][right,r]
        myqsort(nums, l, left);
        myqsort(nums, right, r);
        return;
    }
};

1.3数组中第K个最大元素

215. 数组中的第K个最大元素 - 力扣(LeetCode)

这道题在每次排序前,需要根据区间长度判断这个元素在哪个区间,对于划分出的三个区间,abc,a区间大于基准值,b区间等于基准值,c区间小于基准值

假如a>=K,说明该元素应该存在于a区间

假如a+b>=K,说明满足K>a,那么此时b区间的基准值就是第K个最大元素

而如果K>a+b,此时K就落在c区间

所以只需要对目标区间继续排序查找即可,对于c区间,K要变成K-a-b再进行排序,因为排除了右侧的a+b个元素,那么相当于在c区间查找第K-a-b大的元素

代码部分

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
          srand(time(NULL));
          return myqsort(nums,0,nums.size()-1,k);
    }

    int getRandom(vector<int>& nums,int l,int r){
        return nums[rand()%(r-l+1)+l];
    }
    int myqsort(vector<int>& nums,int l,int r,int k){
        if(l>=r)return nums[l];
        int pos=getRandom(nums,l,r);
        int left=l-1;
        int right=r+1;
        int i=left+1;
        while(i<right){
            if(nums[i]<pos)swap(nums[++left],nums[i++]);
            else if(nums[i]==pos)nums[i++];
            else swap(nums[--right],nums[i]);
        }
        int a=r-right+1;
        int b=right-left-1;
        int c=left-l+1;
        if(a>=k)return myqsort(nums,right,r,k);
        else if(a+b>=k)return pos;
        else return myqsort(nums,l,left,k-a-b);
    }

1.4库存管理III

LCR 159. 库存管理 III - 力扣(LeetCode)

和上一次的思路类似,只不过这次要返回区间,但是这反而更简单了,因为对于区间内部不需要有序,也不用定位到精确的元素

同样是这幅图,这次只需要将右侧作为小的那一侧进行排序即可

然后就是对区间长度进行判断

假如a>cnt,说明区间在a内部,为什么不带等号呢,因为如果a=cnt那么直接返回a区间就行

假如a+b>=cnt,说明区间的左端点在b区间,只要从右侧向左找cnt个元素即可

假如cnt>a+b,说明左端点在c区间,注意仍然需要对cnt进行更改

代码部分

class Solution {
public:
    vector<int> inventoryManagement(vector<int>& stock, int cnt) {
        srand(time(NULL));
        vector<int> ret;
        int len=stock.size();
        myqsort(stock,0,len-1,cnt);
        for(int i=1;i<=cnt;i++){
            ret.push_back(stock[len-i]);
        }
        return ret;

    }
    int getRandom(vector<int>& stock,int l,int r){
        return stock[rand()%(r-l+1)+l];
    }
    int myqsort(vector<int>& stock,int l,int r,int cnt){
        if(l>=r)return l;
        int pos=getRandom(stock,l,r);
        int left=l-1;
        int right=r+1;
        int i=left+1;
        while(i<right){
            if(stock[i]>pos)swap(stock[++left],stock[i++]);
            else if(stock[i]==pos)stock[i++];
            else swap(stock[--right],stock[i]);
        }
        int a=r-right+1;
        int b=right-left-1;
        int c=left-l+1;
        if(a>cnt)return myqsort(stock,right,r,cnt);
        else if(a+b>=cnt)return cnt;
        else return myqsort(stock,l,left,cnt-a-b);
    }
};

2.归并排序

归并排序,就是将数组先分割到最小,然后逐次向上返回,逐渐合并小区间,每次合并时左右区间应当都有序,然后依次将数组元素按顺序合并,这里要借助一个辅助的数组,存储合并的结果,并映射到原数组,将原数组的这部分进行更改

从底下的最小数组逐层向上合并

2.1排序数组

912. 排序数组 - 力扣(LeetCode)

刚才用快排解决了问题,这次用归并的方法,核心就是这一部分代码,每次进入归并,如果左右端点重合就直接返回,如果还可以继续分割数组就继续递归

这里不使用基准值,而是直接将数组对半分开,每个区间都对半分开,然后往下递归,到最下层的时候,也就是找到了最左侧的两个元素,此时开始第一次的合并,因为只有一个元素,所以左右数组已经有序,然后用指针遍历两个数组,按照小的先进,放入临时数组,然后将排序结果映射到原数组

注意当元素多起来的时候,左右数组可能存在当一个数组已经走完,另一个数组还剩下元素的情况,要将剩下元素全部放入临时数组

注意最后的for循环,因为j是从左端点移动到右端点,是原数组实际的下标,而临时数组是从0开始的,要注意下标的控制

代码部分

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        vector<int> tmp(nums.size());
        mymergesort(nums,0,nums.size()-1,tmp);
        return nums;
    }
    void mymergesort(vector<int>& nums,int l,int r,vector<int>& tmp){
        if(l>=r)return;
        int mid=(l+r)/2;
        //[l,mid][mid+1,r]把数组分为两部分继续归并
        mymergesort(nums,l,mid,tmp);
        mymergesort(nums,mid+1,r,tmp);
        int cur1=l;
        int cur2=mid+1;
        int i=0;
        while(cur1<=mid&&cur2<=r){
            if(nums[cur1]<nums[cur2])tmp[i++]=nums[cur1++];
            else tmp[i++]=nums[cur2++];
        }
        while(cur1<=mid)tmp[i++]=nums[cur1++];
        while(cur2<=r)tmp[i++]=nums[cur2++];
        for(int j=l;j<=r;j++){
            nums[j]=tmp[j-l];
        }
        return;
    }
};

2.2交易逆序对的总数

LCR 170. 交易逆序对的总数 - 力扣(LeetCode)

这道题的逆序对可以逐层拆解,也就是对于我们拆开的左右数组,每次的左右区间内部都已经查找完了逆序对并且有序,在合并时查找新的逆序对,从左边挑一个,从右边挑一个组成逆序对

至于为什么我们可以设定左右区间内部有序且查找完逆序对再往上合并,因为当数组分割到只剩一个元素,自然就满足了条件,因为一个元素必定有序且逆序对已经找完,只不过逆序对为0,然后向上返回,开始左右区间的合并与新的逆序对查找

(这里使用升序排序,将右数组作为主数组,左数组用于累加结果)

因为逆序对左边大,所以当左侧的record[cur1]>record[cur2]时,由于数组升序,所以cur1以及右侧所有元素都和当前cur2位置的元素构成逆序对,且因为是新数组合并,必然不重复,并且可以大大提高查找速度,也就是逆序对个数+=right-cur1+1,一次性可以找一串

代码部分,其实代码部分需要改动的不多啊,基本上都是归并排序需要写的基本步骤,只不过在小的判断条件上,小小更改一下就可以了

class Solution {
public:
    int reversePairs(vector<int>& record) {
        vector<int> tmp(record.size());
        int ret=0;
        mymsort(record,0,record.size()-1,ret,tmp);
        return ret;
    }
    void mymsort(vector<int>& record,int left,int right,int& ret,vector<int>& tmp){
        if(left>=right)return;
        int mid=(left+right)/2;
        mymsort(record,left,mid,ret,tmp);
        mymsort(record,mid+1,right,ret,tmp);
        int cur1=left;
        int cur2=mid+1;
        int i=0;
        while(cur1<=mid&&cur2<=right){
            if(record[cur1]<=record[cur2])tmp[i++]=record[cur1++];
            else{
                ret+=(mid-cur1+1);
                tmp[i++]=record[cur2++];
            }
        }
        while(cur1<=mid)tmp[i++]=record[cur1++];
        while(cur2<=right)tmp[i++]=record[cur2++];
        for(int j=left;j<=right;j++){
            record[j]=tmp[j-left];
        }
        return;

    }
};

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

315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)

这道题和上一题的思路类似,只不过这里我们换一个思路,使用降序,然后从前一个数组中取大的元素,也就是将左数组作为主数组,从右数组中累加结果

如果是升序,假设此时cur1的位置元素比cur2要大,因为是升序,只知道cur2以及左侧元素均小于cur1的元素,但是此时cur2进行移动之后,新的元素是不是一定比cur1位置的小呢,这是未知的,也就是不能一次性确定一串满足条件的元素个数进行累加,也就是这次判断对累加结果没有作用

但是使用降序的话,同样的情况,可以确定cur2以及右侧元素一定是比cur1位置元素小的,所以主数组的选择对升降序的要求也不一样

注意这道题要返回的是一个数组,每个位置对应多少个结果,要一一返回出来,这就需要多一个辅助数组,用于绑定每一个元素的下标,只要每次合并数组时,对下标数组也进行一样的操作,就可以通过元素获取到它本来的下标,因为最终的返回数组,是按照最开始没有排序的位置给每个位置加上对应满足元素的个数,所以记录原下标是很有必要的

代码部分,代码部分看着很多,其实需要注意细节的地方,就那么几个,大部分依旧是归并的基础代码,这里只是加上了对下标数组的同步操作,注意辅助数组也要多加一个

vector<int> tmp1(100005);
vector<int> tmp2(100005);
class Solution {
public:
    vector<int> countSmaller(vector<int>& nums) {
        int len = nums.size();
        vector<int> index(len);
        vector<int> result(len);
        for (int i = 0; i < len; i++) {
            index[i] = i;
        }
        // 三个数组,tmp用于归并的临时数组
        // index绑定元素和下标
        // result是结果数组
        mymsort(nums, 0, len - 1, index, result);
        return result;
    }
    void mymsort(vector<int>& nums, int left, int right, vector<int>& index,
                 vector<int>& result) {
        if (left >= right)
            return;
        int mid = (left + right) / 2;
        mymsort(nums, left, mid, index, result);
        mymsort(nums, mid + 1, right, index, result);
        int cur1 = left;
        int cur2 = mid + 1;
        int i = 0;
        while (cur1 <= mid && cur2 <= right) {
            if (nums[cur1] > nums[cur2]) {
                tmp1[i] = nums[cur1];
                tmp2[i] = index[cur1];
                result[index[cur1]] += (right - cur2 + 1);
                //通过下标数组定位位置
                i++;
                cur1++;
            } else {
                tmp1[i] = nums[cur2];
                tmp2[i] = index[cur2];
                i++;
                cur2++;
            }
        }
        while (cur1 <= mid) {
            tmp1[i] = nums[cur1];
            tmp2[i] = index[cur1];
            i++;
            cur1++;
        }
        while (cur2 <= right) {
            tmp1[i] = nums[cur2];
            tmp2[i] = index[cur2];
            i++;
            cur2++;
        }
        for (int j = left; j <= right; j++) {
            nums[j] = tmp1[j - left];
            index[j] = tmp2[j - left];
        }
        return;
    }
};

2.4翻转对

493. 翻转对 - 力扣(LeetCode)

这道题就是把条件稍微改了一下,将逆序对的条件改成了大于两倍才行,代码部分需要改动的不多,对于左右的有序数组,遍历两次即可,第一次累加翻转对的对数,但是对数组不进行合并,因为这个条件如果直接拿去排序,不能保证新数组有序,所以我们将找翻转对和合并数组分两步进行即可

代码部分

vector<int> tmp(50005);
class Solution {
public:
    int reversePairs(vector<int>& nums) {
        int len = nums.size();
        int ret = 0;
        mymsort(nums, 0, len - 1, ret);
        return ret;
    }
    void mymsort(vector<int>& nums, int left, int right, int& ret) {
        if (left >= right)
            return;
        int mid = (left + right) / 2;
        mymsort(nums, left, mid, ret);
        mymsort(nums, mid + 1, right, ret);
        int cur1 = left;
        int cur2 = mid + 1;
        int i = 0;
        while (cur1 <= mid && cur2 <= right) {
            if (nums[cur1]/2.0 > nums[cur2]) {
                ret += (right - cur2 + 1);
                cur1++;
            } else
                cur2++;
        }
        cur1 = left;
        cur2 = mid + 1;
        while (cur1 <= mid && cur2 <= right) {
            if (nums[cur1] > nums[cur2]) {
                tmp[i++] = nums[cur1++];
            } else
                tmp[i++] = nums[cur2++];
        }
        while (cur1 <= mid)
            tmp[i++] = nums[cur1++];
        while (cur2 <= right)
            tmp[i++] = nums[cur2++];
        for (int j = left; j <= right; j++) {
            nums[j] = tmp[j - left];
        }
        return;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值