具体来说,分治算法包含以下几个步骤:
分治算法通常适用于能够被划分成相互独立子问题的问题,并且这些子问题的结构和原问题一样。经典的分治算法有许多,如归并排序、快速排序、二分搜索等。
经典例子:归并排序
分治算法的优点包括:
题目链接:https://leetcode.cn/problems/sort-colors/
给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0
、 1
和 2
分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例 1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例 2:
输入:nums = [2,0,1]
输出:[0,1,2]
提示:
n == nums.length
1 <= n <= 300
nums[i]
为 0
、1
或 2
思路
具体的思路可以分为以下三个部分:
left
的左侧都是红色元素。初始时,left
设置为-1。i
向右移动,不进行交换。白色元素已经排列在红色元素的右侧,所以不需要额外操作。right
的右侧都是蓝色元素。初始时,right
设置为数组的长度。整个过程在遍历指针 i
小于右边界 right
的情况下进行。当 i
与 right
相遇时,排序完成。
代码
class Solution {
public:
void sortColors(vector<int>& nums) {
for(int i=0,left=-1,right=nums.size();i<right;){
if(nums[i]==0) swap(nums[++left],nums[i++]);
else if(nums[i]==1) i++;
else swap(nums[i],nums[--right]);
}
}
};
题目链接:https://leetcode.cn/problems/sort-an-array/
给你一个整数数组 nums
,请你将该数组升序排列。
示例 1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例 2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
提示:
1 <= nums.length <= 5 * 104
-5 * 104 <= nums[i] <= 5 * 104
思路
普通快排在这里是通过不了的,所以我们可以使用上面颜色分类的思想进行三路划分的优化
三路划分是对传统快速排序算法的一种改进,通过将数组划分为三个部分:小于、等于、大于基准值,从而在存在大量相同元素的情况下,提高了性能。
传统快速排序在处理有大量相同元素的数组时可能会导致不均匀的划分,使得递归树不平衡,进而影响性能。三路划分通过在划分过程中将数组分为小于、等于、大于基准值的三个部分,有效地解决了这一问题,具有以下优势:
三路划分的核心思想是通过一个循环,将数组划分为小于、等于、大于基准值的三个部分。这样,相同元素被聚集在等于基准值的部分,从而在递归过程中能够更高效地处理重复元素。这一优化使得算法在处理包含大量相同元素的数组时,性能更为稳定。
代码
class Solution {
public:
int getRandom(vector<int>& nums,int left, int right){
return nums[rand()%(right-left+1)+left];
}
void qsort(vector<int>& 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(i<right){
if(nums[i]<key) swap(nums[++left],nums[i++]);
else if(nums[i]==key) i++;
else swap(nums[--right],nums[i]);
}
qsort(nums,l,left);
qsort(nums,right,r);
}
vector<int> sortArray(vector<int>& nums) {
srand(time(NULL));
qsort(nums,0,nums.size()-1);
return nums;
}
};
题目链接:https://leetcode.cn/problems/kth-largest-element-in-an-array/
给定整数数组 nums
和整数 k
,请返回数组中第 **k**
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4
提示:
1 <= k <= nums.length <= 105
-104 <= nums[i] <= 104
思路
这里最常规的写法应该是使用堆排,但是这样达不到O(n)的时间复杂度,所以这里我们结合快排中的三路划分思想
代码
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
srand(time(NULL)); // 设置随机数种子
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];
// 1. 随机选择基准元素
int key = getRandom(nums, l, r);
// 2. 根据基准元素将数组分为三块
int left = l - 1, right = r + 1, i = l;
while (i < right) {
if (nums[i] < key) {
swap(nums[++left], nums[i++]);
} else if (nums[i] == key) {
i++;
} else {
swap(nums[--right], nums[i]);
}
}
// 3. 分情况讨论
int c = r - right + 1, b = right - left - 1;
if (c >= k) {
// 第 k 大元素在右侧部分
return qsort(nums, right, r, k);
} else if (b + c >= k) {
// 第 k 大元素等于基准元素
return key;
} else {
// 第 k 大元素在左侧部分
return qsort(nums, l, left, k - b - c);
}
}
int getRandom(vector<int>& nums, int left, int right) {
return nums[rand() % (right - left + 1) + left];
}
};
c
表示右侧部分元素的个数,即大于基准元素的个数。b
表示基准元素左侧部分元素的个数,即等于基准元素的个数。c
大于等于 k,说明第 k 大元素在右侧部分。因此,递归地在右侧部分中继续寻找第 k 大元素。b + c
大于等于 k,说明第 k 大元素等于基准元素。此时,基准元素即为所求的第 k 大元素,直接返回基准元素的值。k
减去右侧和基准元素的个数。这样的划分和递归过程保证了在不同情况下都能正确地找到第 k 大元素,从而完成整个算法。这是随机化快速排序在选择第 k 大元素时的一种处理策略,通过考虑基准元素左右两侧的元素个数,提高了算法在寻找第 k 大元素时的效率。
题目链接:https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/
仓库管理员以数组 stock
形式记录商品库存表,其中 stock[i]
表示对应商品库存余量。请返回库存余量最少的 cnt
个商品余量,返回 顺序不限。
示例 1:
输入:stock = [2,5,7,4], cnt = 1
输出:[2]
示例 2:
输入:stock = [0,2,3,6], cnt = 2
输出:[0,2] 或 [2,0]
提示:
0 <= cnt <= stock.length <= 10000 0 <= stock[i] <= 10000
思路
这一题和上一题的思路基本一致,同样我们使用快速选择的算法,可以使时间复杂度达到O(n),只不过需要简单做一些调整
代码
class Solution {
public:
void qsort(vector<int>& nums, int l, int r, int k) {
if (l >= r) return;
// 随机选择基准元素
int key = nums[rand() % (r - l + 1) + l];
int left = l - 1, right = r + 1, i = l;
// 划分过程
while (i < right) {
if (nums[i] < key) {
swap(nums[++left], nums[i++]);
} else if (nums[i] == key) {
i++;
} else {
swap(nums[--right], nums[i]);
}
}
int a = left - l + 1, b = right - left - 1;
// 根据划分情况递归处理
if (a > k) {
// 第 k 小元素在左侧部分
qsort(nums, l, left, k);
} else if (a + b >= k) {
// 第 k 小元素在基准元素右侧,且可能包含部分基准元素
return;
} else {
// 第 k 小元素在右侧部分
qsort(nums, right, r, k - a - b);
}
}
vector<int> inventoryManagement(vector<int>& stock, int cnt) {
srand(time(NULL));
// 调用随机化快速排序
qsort(stock, 0, stock.size() - 1, cnt);
// 返回前 cnt 小的商品
return {stock.begin(), stock.begin() + cnt};
}
};
题目链接:https://leetcode.cn/problems/sort-an-array/
给你一个整数数组 nums
,请你将该数组升序排列。
示例 1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例 2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
提示:
1 <= nums.length <= 5 * 104
-5 * 104 <= nums[i] <= 5 * 104
思路
要理解分治中的归并思想,首先我们从归并排序入手,这里我直接编写代码,想看更清晰的排序剖析,可以翻看博主之前关于八大排序的博客
代码
class Solution {
vector<int> 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;
// 计算中间位置
int mid = (right + left) >> 1;
// 递归对左右两部分进行归并排序
mergeSort(nums, left, mid);
mergeSort(nums, mid + 1, right);
// 归并合并两个有序部分
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];
}
};
题目链接:https://leetcode.cn/problems/shu-zu-zhong-de-ni-xu-dui-lcof/
在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record
,返回其中存在的「交易逆序对」总数。
示例 1:
输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9, 7), (9, 5), (9, 4), (9, 6), (7, 5), (7, 4), (7, 6), (5, 4)。
限制:
0 <= record.length <= 50000
思路
这里我们使用归并的思想可以对数组边排序边进行逆序对的计算,我们在进行归并排序划分时,左边和右边都是相对有序的,我们在归并时,找到了左边相对右边大的那个数,就可以进行一次逆序对的组合,即此时左边被遍历的数及其之后的数都能和此时右边的数进行逆序匹配,此时我们累加逆序对的值,直到我们把整个数组归并完毕,逆序对的总数也就计算完毕了
代码
class Solution {
int tmp[50000];
public:
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) >> 1;
// 递归对左右两部分进行归并排序
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 i = left; i <= right; ++i)
nums[i] = tmp[i - left];
return ret;
}
};
题目链接:https://leetcode.cn/problems/count-of-smaller-numbers-after-self/
给你一个整数数组 nums
,按要求返回一个新数组 counts
。数组 counts
有该性质: counts[i]
的值是 nums[i]
右侧小于 nums[i]
的元素的数量。
示例 1:
输入:nums = [5,2,6,1]
输出:[2,1,1,0]
解释:
5 的右侧有 2 个更小的元素 (2 和 1)
2 的右侧仅有 1 个更小的元素 (1)
6 的右侧有 1 个更小的元素 (1)
1 的右侧有 0 个更小的元素
示例 2:
输入:nums = [-1]
输出:[0]
示例 3:
输入:nums = [-1,-1]
输出:[0,0]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
思路
我们可以继续利用上面的逆序对思想,只不过我们需要使用额外的数组来记录相对下标。
代码
class Solution {
vector<int> ret;
vector<int> index;
int tmp[500000];
int tindex[500000];
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;
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]){
tmp[i]=nums[cur2];
tindex[i++]=index[cur2++];
}
else{
ret[index[cur1]]+=right-cur2+1;
tmp[i]=nums[cur1];
tindex[i++]=index[cur1++];
}
}
while(cur1<=mid){
tmp[i]=nums[cur1];
tindex[i++]=index[cur1++];
}
while(cur2<=right){
tmp[i]=nums[cur2];
tindex[i++]=index[cur2++];
}
for(int j=left;j<=right;j++){
nums[j]=tmp[j-left];
index[j]=tindex[j-left];
}
}
};
题目链接:https://leetcode.cn/problems/reverse-pairs/
给定一个数组 nums
,如果 i < j
且 nums[i] > 2*nums[j]
我们就将 (i, j)
称作一个*重要翻转对*。
你需要返回给定数组中的重要翻转对的数量。
示例 1:
输入: [1,3,2,3,1]
输出: 2
示例 2:
输入: [2,4,3,5,1]
输出: 3
注意:
50000
。思路
总体思路依旧是使用归并,我们在每次排序前,找到当前的左边某个数大于右边的两倍,即可一次性计算该数后面的翻转对个数,数组排序完成,即可计算全部的翻转对
代码
class Solution {
int tmp[50000];
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;
int mid=(left+right)>>1;
ret+=mergeSort(nums,left,mid);
ret+=mergeSort(nums,mid+1,right);
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++;
}
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;
}
};