专题七_分治_快排_归并_算法专题详细总结

news/2024/12/24 2:45:45/

目录

分治

一、分治思想的概念

二、分治思想的步骤

1. 颜⾊分类(medium)

解析:

2. 快速排序(medium)

解析:

总结:

3. 快速选择算法(medium)

解析:

1)暴力:

2)优化:快速选择算法

总结:

4. 最⼩的 k 个数(medium)

解析:

1)暴力:

2)优化:快速选择排序

总结:

分治 - 归并排序

5. 归并排序(medium)

解析:

1)快排前面专题讲的很清楚,可以试试

2)堆排

3)归并排序

6. 归并排序(medium)

​编辑解析:

1)暴力:

2)优化:

1.左半部分跳完->右半部分->一左一右​编辑

2.左半部分跳完->左排序->右半部分跳完->右排序->一左一右 + 排序

3.通过2的排序,此时已经变成了一个归并排序来解决问题。

4.拓展:

总结:

7. 计算右侧⼩于当前元素的个数(hard)

解析:

1)暴力:

2)优化:分治_归并

总结:

8. 翻转对(hard)

解析:

归并排序:


分治

一、分治思想的概念

分治,即 “分而治之”。其核心思想是将一个复杂的问题分成若干个规模较小、相互独立且与原问题形式相同的子问题,分别求解这些子问题,然后将子问题的解合并起来,得到原问题的解。

二、分治思想的步骤

  1. 分解(Divide):

    • 将原问题分解为若干个规模较小、相互独立的子问题。这些子问题的形式与原问题相同,只是规模更小。
    • 例如,在归并排序中,将待排序的数组分成两个规模大致相等的子数组。
  2. 解决(Conquer):

    • 递归地求解各个子问题。如果子问题的规模足够小,则直接求解。
    • 对于归并排序,对子数组继续进行划分,直到子数组的长度为 1 或 2 时,直接进行排序。
  3. 合并(Combine):

    • 将子问题的解合并成原问题的解。
    • 在归并排序中,将两个已排序的子数组合并成一个有序的数组。

将一个大问题分解成许多个小问题,然后这些小问题在一一进行解决后合并,这种思想就像快排一一,是一个很好的切入点。

先来一道简单的例题,理解一下分治的简单思想,数组分三块,这里跟前面专题移动零一样运用双指针,这里多加了一个指针,也可以称为三指针,其中mid指针来分别将当前数组的元素跟left right位置的元素进行比较,然后进行交换。简单的分治思想将数组分三块,这里了解后,后面运用数组分三块思想真的很常见。

1. 颜⾊分类(medium)

题目意思就是让我们把数组有序排列,数组里面只有0 1 2三种元素

解析:

那么最开始想到的肯定就是三指针办法,定义left=-1 , mid=0 ,right=n,三个指针然后开始遍历数组,将数组分成三块

这里的数组就包含3种元素,0,1,2,那么1就可以当作是key值来让它跟nums[mid]进行比较。

如果nums[mid] < key , 就说明nums[mid]==0,要往前进行交换,那么就让left先往前走一步,两者就开始进行交换 mid就在往后走一步,即

if(nums[mid]<1) swap(nums[++left],nums[mid++]); 

如果nums[mid]==1,就证明此时mid指针正走在中间区域,那么就直接跳过就好,这又就是为什么要进行数组分三块,只有这样,才能将快排的时间复杂度降低,否则数组分两块,当整个数组全是重复元素的时候,时间复杂度就又退化到O(N^2) ,而数组分三块却只是O(N),只用遍历一遍。

else if(nums[mid]==1) mid++;

最后如果nums[mid]>key 那么就要跟最后面的right进行交换,先--right,防止越界,交换后,mid不能动,因为不能保证交换过来的数字是否满足条件,所以要重新判断

else swap(nums[--right],nums[mid]);

class Solution {
public:void sortColors(vector<int>& nums) {int left=-1,right=nums.size(),mid=0;while(mid<right){if(nums[mid]<1) swap(nums[++left],nums[mid++]);else if(nums[mid]==1) mid++;else swap(nums[--right],nums[mid]);}}
};

总结:

相对来说这题比较简单,数据量也比较少,只用考虑 0 1 2的位置关系

了解数组分三块后,我们就开始进行分治,可能我们只进行一趟,不能够满足我们的条件,只是把==key的值放在它正确的位置,而左右两边只是无序的小于key值 或大于key值,那么我们进行递归操作,就可以完成整个数组的排序任务。

2. 快速排序(medium)

题意很简单,就是在O(nlogn)下完成排序,那么首先想到的就是快排,也是本章分治思想。

解析:

这里分治思想体现的淋漓尽致,那么我们只需要想上一题一样,数组分三块,然后进行排序交换,后面再递归给出我的子数组的范围,只要达到结束条件(left>=right) 即可返回整个数组。

快排,数组分三块思想,这样可以将数组分两块的不足给避免
当数组分两块:如果数组里面全部都是相同的元素,那么时间复杂度就会降低到O(N^2)

当数组分三块,那么数组内就会变成【小于key值的元素】  【==key值的元素】  【大于key值的元素】  那么当数组内全部都是相同元素的时候,就可以只移动mid指针,之对数组进行一次遍历,时间复杂度为O(N^2)  当一趟排序完成后,那么就进行递归,因为给的范围是【l,r】 那么要创建移动的 指针left,right,mid,来表示left和right最终移动的位置,然后就可以得出递归的范围,因为【left+1,right-1】 是==key的元素,那么【l,left】 【right,r】 的两个范围就要继续进行递归完成

最后就是随机数,用srand(time(NULL)) 来种下一颗随机数的种子,然后获取随机数int r=rand() 然后以免取得的随机数下标越界,那么就要对小标进行控制在[l,r]之间r%(right-left+1)+left

class Solution {
public:vector<int> sortArray(vector<int>& nums) {srand(time(NULL)); //开始随机qsort(nums,0,nums.size()-1);return nums;}void qsort(vector<int>& nums,int l,int r){if(l>=r) return;//定义三个指针int left=l-1,right=r+1,mid=l;//获得随机数int key=getRand(nums,l,r);while(mid<right){if(nums[mid]<key) swap(nums[++left],nums[mid++]);else if(nums[mid]==key) mid++;else swap(nums[--right],nums[mid]);}//递归//[l,left] [left+1,right-1] [right,r]qsort(nums,l,left);qsort(nums,right,r);}int getRand(vector<int>& nums,int left,int right){int r=rand();return nums[r%(right-left+1)+left];}
};

总结:

这个代码真的很完美强烈建议默写一百遍!随机取值,数组分三块~

3. 快速选择算法(medium)

这题也是本个章节的重点,topK问题

解析:

1)暴力:

肯定就是先将数组完全排序好后,然后再返回第k大的元素,但是题目要求O(N) 所以如果排序就成了O(NlogN)

2)优化:快速选择算法

快速选择算法也可以认为是快排的一种小变形,只是返回的是特定的值或者区间,所以没有必要完全将数组排完序后再进行返回,那么这样就可以有效的降低时间复杂度

求出数组中第K个最大元素,在时间复杂度达到O(N)的条件下,首先就该考虑快排,将数组分三块,能够很接近O(N) 

那么这一题只是求出数组第K个最大的元素,只要返回那一个值就行,那么最开始我就是运用快排,然后返回k大的下标,后来发现确实也可以不用全部都排完序后在返回,可以排完k大的数后直接返回即可。

数组分三块【<key】 【==key】  【>key】 
1) 当大于key的数组里面排序在[right,r]之间数字个数为k的时候就直接返回
2)当大于key的数字个数不够k个,那就再加上==key的数字个数大于等于k个的时候就返回key
3)当==key和>key的数字个数和都小于k,那么就要到<key里面去找满足条件的,
 

class Solution {
public:int findKthLargest(vector<int>& nums, int k) {srand(time(nullptr));return qsort(nums,0,nums.size()-1,k);}int qsort(vector<int>& nums,int l,int r,int k){if(l>=r) return nums[l];int left=l-1,right=r+1,mid=l;int key=getRand(nums,l,r); while(mid<right){if(nums[mid]<key) swap(nums[++left],nums[mid++]);else if(nums[mid]==key) mid++;else swap(nums[--right],nums[mid]);}//分情况讨论:int c=r-right+1,b=right-left-1;if(c>=k) return qsort(nums,right,r,k); //缩小排序范围else if(b+c>=k) return key;else return qsort(nums,l,left,k-b-c);}int getRand(vector<int>& nums,int left,int right){int r=rand();return nums[r%(right-left+1)+left];}
};

总结:

这题是跟上题快排大差不差,唯一不同就是再最后的递归部分,考虑清除边界条件

b=right - 1 - (left + 1) +1 = right - left - 1;

//分情况讨论:
        int c=r-right+1,b=right-left-1;
        if(c>=k) return qsort(nums,right,r,k);
        else if(b+c>=k) return key;
        else return qsort(nums,l,left,k-b-c);

4. 最⼩的 k 个数(medium)

这一题仍然是TopK问题,那么这题正好跟上一题相反,找到最小的k个数

解析:

1)暴力:

仍然就是快排后返回数组

2)优化:快速选择排序

快速选择排序,仍然是topK问题,不用完全排序只需要排序排个大概,了解大概元素的分布,前几个元素在哪,然后直接返回,时间复杂度O(N),但是其实我觉得还是排序方便O(NlogN) 只要你排好序了,想怎么返回就怎么返回

class Solution {
public:vector<int> inventoryManagement(vector<int>& nums, int k) {srand(time(nullptr));qsort(nums,0,nums.size()-1,k);return {nums.begin(),nums.begin()+k};}void qsort(vector<int>& nums,int l,int r,int k){if(l>=r) return;int left=l-1,right=r+1,mid=l;int key=getRand(nums,l,r);while(mid<right){if(nums[mid]<key) swap(nums[++left],nums[mid++]);else if(nums[mid]==key) mid++;else swap(nums[--right],nums[mid]);}//求[<key] [==key] [>key] 的个数int a=left-l+1,b=right-left-1;if(a>=k) qsort(nums,l,left,k);else if(a+b>=k) return;else qsort(nums,right,r,k-a-b);}int getRand(vector<int>& nums,int left,int right){return nums[rand()%(right-left+1)+left];}
};

总结:

想着一系列TopK问题都是利用数组分三块,然后在递归部分来进行缩小范围,直到满足结束条件,最后终止。

分治 - 归并排序

5. 归并排序(medium)

题目意思就是排序这个数组,但是我们这个专题采用归并的思想来进行,将大数组分成小数组,然后合并已经排好序的小数组。

解析:

1)快排
前面专题讲的很清楚,可以试试

2)堆排

也是很不错的排序O(NlogK)

3)归并排序

将大数组分成一小份一小份的小数组,然后对小数组进行排序,再将两个小数组进行归并,一直取较小的值合并到原数组中。

然后对没有排完的数组进行剩余的加入,最后再将排好序数组放入到原数组里。

 //快排版

class Solution {
public:vector<int> sortArray(vector<int>& nums) {srand(time(NULL)); //开始随机qsort(nums,0,nums.size()-1);return nums;}void qsort(vector<int>& nums,int l,int r){if(l>=r) return;//定义三个指针int left=l-1,right=r+1,mid=l;//获得随机数int key=getRand(nums,l,r);while(mid<right){if(nums[mid]<key) swap(nums[++left],nums[mid++]);else if(nums[mid]==key) mid++;else swap(nums[--right],nums[mid]);}//递归//[l,left] [left+1,right-1] [right,r]qsort(nums,l,left);qsort(nums,right,r);}int getRand(vector<int>& nums,int left,int right){int r=rand();return nums[r%(right-left+1)+left];}
};

//归并版

class Solution {
public:vector<int> sortArray(vector<int>& nums) {mergeSort(nums,0,nums.size()-1);return nums;}void mergeSort(vector<int>& nums,int left,int right){if(left>=right) return;int mid=(left+right)/2;mergeSort(nums,left,mid);mergeSort(nums,mid+1,right);vector<int> tmp(right-left+1);//合并两个有序数组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++];//还原for(int i=left;i<=right;i++) nums[i]=tmp[i-left];//tmp从0开始计数}
};

6. 归并排序(medium)

求解数组里面前一个数大于后一个数的 对数。

解析:

1)暴力:

就是两层for循环来暴力枚举所有两个数之间的情况,符合ret++,但是肯定会超时

2)优化:

先将数组分两块,

1.左半部分跳完->右半部分->一左一右

2.左半部分跳完->左排序->右半部分跳完->右排序->一左一右 + 排序

3.通过2的排序,此时已经变成了一个归并排序来解决问题。

算法原理:数组程升序状态

策略一:找出该数之前,有多少个数比我大

4.拓展:

为什么这题要用升序数组?不能用降序数组吗?降序数组就没有作用吗?

那么思考再什么情况下利用降序比较合理呢?

策略二:找出该数之后,有多少个数比我小(适合用降序)

因为采用降序,nums[cur1] 是第一次比nums[cur2]要大的,所以就完全可以计算出cur2后面所有元素的个数,因为cur2 前面的元素都要比cur1要大。

class Solution {
public:int tmp[50001];int reversePairs(vector<int>& record) {return mergeSort(record,0,record.size()-1);}int mergeSort(vector<int>& nums,int left,int right){if(left>=right) return 0;int ret=0;int mid=(left+right)/2;//1.左统计+左排序ret+=mergeSort(nums,left,mid);//2.右统计+右排序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+=mid-cur1+1;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 ret;}
};

总结:

首先由数组能不能分成小块进行引入,如果可以,那么就分小块进行,这样讲整体细化成一个个小问题。再从这里想到对数组划分后能不能进行排序,想到分治归并上。主要就是要明白再挑选一左一右统计次数的时候要分清什么情况下是可以一次统计完所有次数的,大体思路就跟归并没什么两样。

7. 计算右侧⼩于当前元素的个数(hard)

读完题发现这题简直跟上一题求逆序对的个数一模一样,唯一的难点就是要求,求出每一个小于数字的个数然后存入数组内。

解析:

1)暴力:

两层for()循环,肯定会超时的

2)优化:分治_归并

这一题就是运用上一题的策略二:当前元素的后面有多少个比我小的元素

这里需要讲nums数组的下标进行绑定到index数组里面

class Solution {vector<int> ret;vector<int> index; // 记录 nums 中当前元素的原始下标int tmpNums[500010];int tmpIndex[500010];public:vector<int> countSmaller(vector<int>& nums) {int n = nums.size();ret.resize(n);index.resize(n);// 初始化⼀下 index 数组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;int mid = (left + right) >> 1;// 先搞搞左边,在搞搞右边mergeSort(nums, left, mid);mergeSort(nums, mid + 1, right);// 处理一左一右的情况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 {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++];}// 还原数组for (int j = left; j <= right; j++) {nums[j] = tmpNums[j - left];index[j] = tmpIndex[j - left];}}
};

总结:

这题跟上一题大差不差,唯一不同的就是要多创建一个数组index来绑定nums数组的下标。

总结:归并排序里面如果不涉及返回左边元素大于右边元素的具体个数,那么就不用考虑升序降序;如果涉及了考虑左边元素的某一个具体元素要比右边元素大多少个,那么就只能用降序,因为降序才包含right-cur2+1;但是升序就只包含mid-cur1+1,这个是对于右边元素大于nums[cur2] 这个数的次数,所以不是小于nums[cur1]的次数,即不一样
 

8. 翻转对(hard)

这一题跟逆序对超级一样,题目意思就是说前面的数要大于后面数的两倍

解析:

归并排序:

在归并排序的过程中,假设对于数组 nums[l..r] 而言,我们已经分别求出了子数组 nums[l..m] 与 nums[m+1..r] 的翻转对数目,并已将两个子数组分别排好序,则 nums[l..r] 中的翻转对数目,就等于两个子数组的翻转对数目之和,加上左右端点分别位于两个子数组的翻转对数目。

我们可以为两个数组分别维护指针 i,j。对于任意给定的 i 而言,我们不断地向右移动 j,直到 nums[i]≤2⋅nums[j]。此时,意味着以 i 为左端点的翻转对数量为 j−m−1。随后,我们再将 i 向右移动一个单位,并用相同的方式计算以 i 为左端点的翻转对数量。不断重复这样的过程,就能够求出所有左右端点分别位于两个子数组的翻转对数目。

class Solution {
public:int tmp[50001];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 mid=(left+right)/2;int ret=0;ret+=mergeSort(nums,left,mid);ret+=mergeSort(nums,mid+1,right);//一左一右int cur1=left,cur2=mid+1;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;int i=0;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 j=left;j<=right;j++) nums[j]=tmp[j-left];return ret;}
};

翻转堆
总结:
1.题目意思就是求数组里面有多少个当前的数字大于我后面的数字对,如果采用暴力枚举,就是O(N^2) 肯定会超时,那么就要想办法优化这个选数的规则

2.首先就是从排序下手,因为只有排序才会尽可能优化我们的算法,一次性求出多个满足条件的翻转对,就比如有序的情况下,利用下标相减,就可以直接得出哪两个数之间,是完全满足条件的,通过下标相减就可以得出

3.但是一般的比较快的排序都是不稳定的,唯独只有归并排序,他是稳定的,那么就可以从这里下手,第一,归并它稳定。第二归并它的效率高,时间复杂度是O(NlogN)

4.想好从归并后就可以开始举列子,康康是不是归并真的满足条件,那么就把数组分两块,可以先考虑降序的情况,当两块数组呈现降序的条件时,以cur1为主,来移动cur2,在cur2移动的过程中,nums[cur2]一直减小,直到某一个值时,nums[cur1]>nums[cur2]了,这个时候就可以开始计算满足条件的ret了

5.因为cur2左边的值都比nums[cur2]要小,但是nums[cur2]<nums[cur1] ,那么就说明【cur2,right】 这个区间的值都是小于nums[cur1]的,那么ret+=right-cur+1,就可以一次性判断此时nums[cur1]代表的值满足的所有条件

6.那么我们后面只需要再次移动cur1即可,移动后,可能又会出现nums[cur1]<nums[cur2]的情况,那么就只需要cur2++即可,让cur2不断跳过这些大于nums[cur1]的值,然后在开始计算下一个满足条件的nums[cur1]

7.上面是不同于其他的归并,这里讲合并数组给分开了,后面在进行两个有序数组的合并,从新让cur1 =left, cur2=mid+1,i=0,然后就是老方法进行数组合并,由于这题是降序,那么就把大的数先放进去,然后再将没有访问完的数组一次性放进去,最后就是还原 原数组nums 返回ret即可


http://www.ppmy.cn/news/1530782.html

相关文章

Ai产品经理

从通用类产品经理转行到智能硬件产品经理&#xff0c;虽然都是产品经理&#xff0c;但是即跨越行业也跨越了职业类别。通用类的产品经理工作一般都会完整的经历市场调研、用户研究、产品策略、商业模式、营销、运营以及其他一系列相关的产品管理。转型到智能硬件行业&#xff0…

LVS与nginx的区别

文章目录 一、优势比较二、配置复杂度三、工作层次 一、优势比较 性能: LVS&#xff1a;LVS是专门为高性能设计的&#xff0c;它使用内核级别的数据包处理能力&#xff0c;能够提供非常高的吞吐量和低延迟。Nginx&#xff1a;Nginx也具有出色的性能&#xff0c;特别是在处理静态…

Django后台管理复杂模型

【图书介绍】《Django 5企业级Web应用开发实战&#xff08;视频教学版&#xff09;》_django 5企业级web应用开发实战(视频教学版)-CSDN博客 《Django 5企业级Web应用开发实战&#xff08;视频教学版&#xff09;》(王金柱)【摘要 书评 试读】- 京东图书 (jd.com) Django框架…

网络安全的方方面面

目录 一、网络安全概述二、数据加密三、消息完整性与数字签名四、身份认证五、密钥分发中心(KDC)与证书认证(CA)六、防火墙与入侵检测系统七、网络安全协议八、网络安全攻防 -- 黑客攻击简要流程九、网络安全常用术语 一、网络安全概述 网络安全的基本特征&#xff1a;相对性、…

ChatGPT-4模型镜像站对比和【软件开发人员】提示词

AI如今很强大&#xff0c;聊聊天、写论文、搞翻译、写代码、写文案、审合同等等&#xff0c;ChatGPT 真是无所不能~ 作为一款出色的大语言模型&#xff0c;ChatGPT 实现了人类般的对话交流&#xff0c;最主要是能根据上下文进行互动。 接下来&#xff0c;我将介绍 ChatGPT 在…

word2vector训练数据集整理(代码实现)

import math import os import random import torch import dltools from matplotlib import pyplot as plt #读取数据集 def read_ptb():"""将PTB数据集加载到文本行的列表中"""with open(./ptb/ptb.train.txt) as f:raw_text f.read()return…

Spring Cloud Gateway 之动态uri 自定义过滤器

背景&#xff1a;第三方公司 请求本公司入参和出参一样的同一个接口&#xff0c;根据业务类型不一样需要不同业务微服务处理 &#xff0c;和第三方公司协商在请求头中加入业务类型方便我公司在网关成分发请求。 1&#xff1a;在spring cloud gateway yml 中加入路由 重点是 -…

使用宝塔部署项目在win上

项目部署 注意&#xff1a; 前后端部署项目&#xff0c;需要两个域名&#xff08;二级域名&#xff0c;就是主域名结尾的域名&#xff0c;需要在主域名下添加就可以了&#xff09;&#xff0c;前端一个&#xff0c;后端一个 思路&#xff1a;访问域名就会浏览器会加载前端的代…