目录
分治
一、分治思想的概念
二、分治思想的步骤
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)
解析:
归并排序:
分治
一、分治思想的概念
分治,即 “分而治之”。其核心思想是将一个复杂的问题分成若干个规模较小、相互独立且与原问题形式相同的子问题,分别求解这些子问题,然后将子问题的解合并起来,得到原问题的解。
二、分治思想的步骤
-
分解(Divide):
- 将原问题分解为若干个规模较小、相互独立的子问题。这些子问题的形式与原问题相同,只是规模更小。
- 例如,在归并排序中,将待排序的数组分成两个规模大致相等的子数组。
-
解决(Conquer):
- 递归地求解各个子问题。如果子问题的规模足够小,则直接求解。
- 对于归并排序,对子数组继续进行划分,直到子数组的长度为 1 或 2 时,直接进行排序。
-
合并(Combine):
- 将子问题的解合并成原问题的解。
- 在归并排序中,将两个已排序的子数组合并成一个有序的数组。
将一个大问题分解成许多个小问题,然后这些小问题在一一进行解决后合并,这种思想就像快排一一,是一个很好的切入点。
先来一道简单的例题,理解一下分治的简单思想,数组分三块,这里跟前面专题移动零一样运用双指针,这里多加了一个指针,也可以称为三指针,其中mid指针来分别将当前数组的元素跟left right位置的元素进行比较,然后进行交换。简单的分治思想将数组分三块,这里了解后,后面运用数组分三块思想真的很常见。
1. 颜⾊分类(medium)
解析:
那么最开始想到的肯定就是三指针办法,定义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)
解析:
这里分治思想体现的淋漓尽致,那么我们只需要想上一题一样,数组分三块,然后进行排序交换,后面再递归给出我的子数组的范围,只要达到结束条件(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)
解析:
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数组的下标。
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 reversePairsRecursive(vector<int>& nums, int left, int right) {
if (left == right) {
return 0;
} else {
int mid = (left + right) / 2;
int n1 = reversePairsRecursive(nums, left, mid);
int n2 = reversePairsRecursive(nums, mid + 1, right);
int ret = n1 + n2;
// 首先统计下标对的数量
int i = left;
int j = mid + 1;
while (i <= mid) {
while (j <= right && (long long)nums[i] > 2 * (long long)nums[j]) j++;
ret += (j - mid - 1);
i++;
}
// 随后合并两个排序数组
vector<int> sorted(right - left + 1);
int p1 = left, p2 = mid + 1;
int p = 0;
while (p1 <= mid || p2 <= right) {
if (p1 > mid) {
sorted[p++] = nums[p2++];
} else if (p2 > right) {
sorted[p++] = nums[p1++];
} else {
if (nums[p1] < nums[p2]) {
sorted[p++] = nums[p1++];
} else {
sorted[p++] = nums[p2++];
}
}
}
for (int i = 0; i < sorted.size(); i++) {
nums[left + i] = sorted[i];
}
return ret;
}
}
int reversePairs(vector<int>& nums) {
if (nums.size() == 0) return 0;
return reversePairsRecursive(nums, 0, nums.size() - 1);
}
};