本期C++算法专题中我们将学习C++的另一个算法思想策略:分治
相关代码已经上传至作者的个人gitee:楼田莉子/C++算法学习,喜欢请点个赞谢谢
目录
分治介绍
[1. 分解(Divide)](#1. 分解(Divide))
[2. 解决(Conquer)](#2. 解决(Conquer))
[3. 合并(Combine)](#3. 合并(Combine))
经典应用场景
分治算法的特点
1、颜色分类
2、排序数组(快速排序)
3、数组中第K个最大的元素
4、最小的K个数字
算法一:排序
算法思路
时间复杂度
算法二:堆
算法思路
时间复杂度
算法三:快速选择
5、排序数组(归并排序)
6、数组中的逆序对
7、计算右侧小于当前元素的个数
8、翻转对
分治介绍
分治(Divide and Conquer)是一种重要的算法设计策略,其核心思想是将一个复杂的问题分解为若干个规模较小但结构与原问题相似的子问题,递归地解决这些子问题,然后将子问题的解合并得到原问题的解。分治算法通常包含三个步骤:
1. 分解(Divide)
将原问题分解为若干个规模较小的子问题。这些子问题应该是相互独立的,并且与原问题形式相同,只是规模更小。例如,在归并排序中,将待排序数组递归地分成两个子数组,直到每个子数组只包含一个元素。
2. 解决(Conquer)
递归地求解子问题。如果子问题的规模足够小,可以直接求解。例如,在归并排序中,当子数组长度为1时,可以直接视为已排序。
3. 合并(Combine)
将子问题的解合并成原问题的解。这一步是分治算法的关键,需要根据具体问题设计合适的合并策略。例如,归并排序在合并两个已排序的子数组时,通过逐个比较元素的大小,将它们按顺序合并为一个有序数组。
经典应用场景
-
归并排序(Merge Sort)
将数组分成两半,递归排序后再合并。时间复杂度为O(n log n),是分治算法的典型应用。
-
快速排序(Quick Sort)
选择一个基准元素,将数组分成小于基准和大于基准的两部分,递归排序。平均时间复杂度为O(n log n)。
-
二分查找(Binary Search)
在有序数组中查找目标元素,每次将搜索范围减半,时间复杂度为O(log n)。
-
大整数乘法(Karatsuba算法)
通过分治策略将大整数乘法分解为更小的乘法问题,显著提高计算效率。
-
最近点对问题
在平面上的点集中找到距离最近的一对点,通过分治法可以将时间复杂度优化到O(n log n)。
分治算法的特点
- 适用条件:问题可以分解为独立的子问题,且子问题的解可以合并为原问题的解。
- 效率:通常能显著降低时间复杂度,如从O(n²)优化到O(n log n)。
- 缺点:递归调用可能导致额外的空间开销,且合并步骤的设计可能较为复杂。
1、颜色分类
算法思想:三指针。类似于这道题:283. 移动零 - 力扣(LeetCode)
1、left标记0区域最右侧、right标记2区域最左侧、i遍历数组
0,left\]:全是0
\[letf+1,i-1\]:全是1
\[i,right-1\]:待扫描的元素
\[right,n-1\]:全是2
2、分类讨论
如果nums\[i\]为0,left++,交换nums\[left\]和nums\[i\],i++;
如果nums\[i\]为1,i++;
如果nums\[i\]为2,right--,交换nums\[right\]和nums\[i\];(因为后面是待扫描的元素,所以i不能++)
```cpp
class Solution {
public:
void sortColors(vector& nums)
{
int n=nums.size();
int left=-1,right=n,i=0;
while(ikey,right--,交换nums\[right\]和nums\[i\];(因为后面是待扫描的元素,所以i不能++)
**优化**:用随机的方式选择key(在《算法导论》中有数学证明,利用概率求期望)
```cpp
class Solution {
public:
//获取随机数
int getRandom(vector&nums,int left,int right)
{
int r_val=rand();
return nums[r_val%(right-left+1)+left];
}
//快排
void qsort(vector&nums,int l,int r)
{
if(l>=r) return ;
//数组分区
int key=getRandom(nums,l,r);
int i=l,left=l-1,right=r+1;
while(ikey) swap(nums[--right],nums[i]);
}
//分成了[l,left][left+1,right-1][right,r-1]
qsort(nums,l,left);
qsort(nums,right,r);
}
//主体函数
vector sortArray(vector& nums)
{
srand(time(NULL));//种下随机数种子
qsort(nums,0,nums.size()-1);
return nums;
}
};
```
## 3、数组中第K个最大的元素

这道题就是之前数据结构堆中Top-K问题[数据结构学习之堆-CSDN博客](https://blog.csdn.net/2401_89119815/article/details/149283070?fromshare=blogdetail&sharetype=blogdetail&sharerId=149283070&sharerefer=PC&sharesource=2401_89119815&sharefrom=from_link "数据结构学习之堆-CSDN博客")
而本期内容我们将介绍它的另外一种算法解决:快速选择算法
算法思想:数组分三块+随机选择基准元素

```cpp
class Solution {
public:
//主题函数
int findKthLargest(vector& nums, int k)
{
srand(time(NULL));
return qsort(nums,0,nums.size()-1,k);
}
//快排
int qsort(vector&nums ,int l,int r,int k)
{
if(l==r) return nums[l];
//随机选择基准元素
int key=getRandom(nums,1,r);
//根据基本元素划分
int i=l,left=l-1,right=r+1;
while(ikey) swap(nums[--right],nums[i]);
}
//分情况讨论
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 getRandom(vector&nums,int left,int right)
{
int r_val=rand();
return nums[r_val%(right-left+1)+left];
}
};
```
## 4、最小的K个数字

算法思想:有三种算法思想:排序、堆、快速选择
本篇重点讲解快速选择算法思想
### 算法一:排序
##### 算法思路
1. 将数组全部排序
2. 取排序后数组的前k个元素
##### 时间复杂度
* O(n log n):主要来自排序操作
```cpp
class Solution {
public:
vector getLeastNumbers_Sort(vector& arr, int k)
{
sort(arr.begin(), arr.end());
return vector(arr.begin(), arr.begin() + k);
}
};
```
### 算法二:堆
##### 算法思路
1. 使用最大堆维护当前最小的k个元素
2. 遍历数组,当堆大小小于k时直接加入
3. 当堆已满时,如果当前元素小于堆顶,则替换堆顶元素
4. 最终堆中元素即为最小的k个数字
##### 时间复杂度
* O(n log k):每个元素最多需要一次堆操作
```cpp
class Solution {
public:
vector getLeastNumbers_Heap(vector& arr, int k)
{
if (k == 0) return vector();
priority_queue maxHeap; // 最大堆
for (int num : arr) {
if (maxHeap.size() < k) {
maxHeap.push(num);
} else if (num < maxHeap.top()) {
maxHeap.pop();
maxHeap.push(num);
}
}
vector result;
while (!maxHeap.empty()) {
result.push_back(maxHeap.top());
maxHeap.pop();
}
return result;
}
};
```
### 算法三:快速选择
随机算法基准值+数组分三块

```cpp
class Solution
{
public:
vector getLeastNumbers_Sort(vector& arr, int k)
{
//快速选择算法
srand(time(NULL));
qsort(arr,0,arr.size()-1,k);
return {arr.begin(),arr.begin()+k};
}
void qsort(vector& arr, int l, int r, int k)
{
if (l >= r) return;
//随机选择基准值
int key = getRandom(arr, l, r);
//数组分三块
int i = l, left = l - 1, right = r + 1;
while (i < right)
{
if (arr[i] < key) swap(arr[++left], arr[i++]);
else if (arr[i] == key) i++;
else if (arr[i] > key) swap(arr[--right], arr[i]);
}
//[l, left][left+1, right-1][right, r]
int a=left-l+1,b=right-left,c=r-right+1;
if(a>key) qsort(arr, l, left, k);
else if(a+b>=key) qsort(arr, left+1, right-1, k-a);
else qsort(arr, right, r, k-a-b);
}
int getRandom(vector& arr, int l, int r)
{
return arr[rand() % (r - l + 1) + l];
}
};
```
## 5、排序数组(归并排序)

算法原理:合并两个有序数组
快排类似于二叉树的前序遍历过程,归并排序类似于二叉树的后序遍历过程
```cpp
//优化前的版本:
//每次都要创建vector,开销比较大
class Solution {
public:
void mergeSort(vectornums, int left, int right)
{
if (left >= right) return;
//选择中间点划分区间
int mid = left + (right-left) / 2;
//递归排序左半区间
mergeSort(nums, left, mid);
//递归排序右半区间
mergeSort(nums, mid + 1, right);
//合并两个有序数组
int cur1 = left,cur2 = mid + 1,i=0;
vectortemp(right - left + 1);
while (cur1 <= mid && cur2 <= right)
temp[i++] = nums[cur1] < nums[cur2] ? nums[cur1++] : nums[cur2++];
//处理没有循环的情况,且只进去一个循环
while (cur1 <= mid) temp[i++] = nums[cur1++];
while (cur2 <= right) temp[i++] = nums[cur2++];
//将temp数组中的元素复制到nums数组中}
for (int i = left; i <= right; i++)
{
nums[i] = temp[i-left];
}
}
//主题函数
vector sortArray(vector& nums)
{
mergeSort(nums, 0, nums.size() - 1);
return nums;
}
};
//优化后的版本:
//递归中频繁创建空间的话最好将对应部分放到全局变量中
class Solution {
public:
vectortemp;
void mergeSort(vectornums, int left, int right)
{
if (left >= right) return;
//选择中间点划分区间
int mid = left + (right - left) / 2;
//递归排序左半区间
mergeSort(nums, left, mid);
//递归排序右半区间
mergeSort(nums, mid + 1, right);
//合并两个有序数组
int cur1 = left, cur2 = mid + 1, i = 0;
while (cur1 <= mid && cur2 <= right)
temp[i++] = nums[cur1] < nums[cur2] ? nums[cur1++] : nums[cur2++];
//处理没有循环的情况,且只进去一个循环
while (cur1 <= mid) temp[i++] = nums[cur1++];
while (cur2 <= right) temp[i++] = nums[cur2++];
//将temp数组中的元素复制到nums数组中}
for (int i = left; i <= right; i++)
{
nums[i] = temp[i - left];
}
}
//主题函数
vector sortArray(vector& nums)
{
temp.resize(nums.size());
mergeSort(nums, 0, nums.size() - 1);
return nums;
}
};
```
## 6、数组中的逆序对

算法思想:
策略1、找出该数之前有多少个数比我大(升序)


策略2、找出该数之后有多少个数比我小(降序)
```cpp
class Solution {
public:
int tmp[50010];
//策略1:升序版本
int reversePairs(vector& nums)
{
return mergeSort(nums, 0, nums.size() - 1);
}
int mergeSort(vector& nums,int left,int right)
{
if (left >= right) return;
int ret = 0;
//找中间位置分为两部分
int mid = (right-left) / 2+left;
//[left,mid][mid+1,right]
//左边个数+排序+右边个数
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
//一左一右个数
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;
}
};
class Solution
{
public:
int tmp[50010];
//策略2:降序版本
int reversePairs(vector& nums)
{
return mergeSort(nums, 0, nums.size() - 1);
}
int mergeSort(vector& nums, int left, int right)
{
if (left >= right) return;
int ret = 0;
//找中间位置分为两部分
int mid = (right - left) / 2 + left;
//[left,mid][mid+1,right]
//左边个数+排序+右边个数
ret += mergeSort(nums, left, mid);
ret += mergeSort(nums, mid + 1, right);
//一左一右个数
int cur1 = left, cur2 = mid + 1, i = 0;
while (cur1 <= mid && cur2 <= right)
{
if (nums[cur1] <= nums[cur2])
{
tmp[i++] = nums[cur2++];
}
else//统计个数
{
ret += mid - cur2 + 1;
tmp[i++] = 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;
}
};
```
## 7、计算右侧小于当前元素的个数

算法思想:找出该数之后有多少个数比我小(降序)

细节问题:nums原始下标是多少?

```cpp
class Solution {
public:
vectorret;
vectorindex;//记录nums中原始下标
int tmpNums[5000010];
int tmpindex[5000010];
vector countSmaller(vector& nums)
{
int n=nums.size();
ret.resize(n);
index.resize(n);
//初始化index
for(int i=0;i&nums,int left,int right)
{
if(left>=right) return;
//根据中间位置划分元素
int mid=left+(right-left)/2;
//[left,mid][mid+1,right]
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];
}
}
};
```
## 8、翻转对

算法思路:类似于前面的逆序对
计算翻转对

```cpp
class Solution {
public:
int tmp[50010];
int reversePairs(vector& nums)
{
return mergeSort(nums, 0, nums.size() - 1);
}
int mergeSort(vector& nums, int left, int right)
{
if(left >= right) return 0;
int ret = 0;
// 1. 先根据中间元素划分区间
int mid = (left + right) >> 1;
// [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 = left;
while(cur1 <= mid) // 降序的情况
{
while(cur2 <= right && nums[cur2] >= nums[cur1] / 2.0) cur2++;
if(cur2 > right)
break;
ret += right - cur2 + 1;
cur1++;
}
// 4. 合并两个有序数组
cur1 = left, cur2 = mid + 1;
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];
return ret;
}
};
```
本期内容就到这里,喜欢请点个赞谢谢