算法——分治
分治即分而治之,将一整个大问题分解成若干个小问题来解决。
一.颜色分类
来看题目(出自力扣):
题目很简单,让我们将一个数组中乱序的颜色排列成红、白、蓝的正确序列。
因为题目中是用数字来代替颜色,因此题目要求不能使用排序函数。
那么我们如何来解决这个问题呢???
根据分治思想,题目要求我们将颜色分为三块,也就是说我们要管理三块区域,因此我们是否能通过去遍历数组,然后将元素放在其应在的区域呢???
如下图所示,我们可以使用三指针,其中 i 指针去遍历数组,如果 i 下标元素为0,就将其放在left维护的左边区域,如果为2,就将其放在right维护的右边区域,而剩余的1,都将会被集中在中间区域。
值得注意的是,i 下标是从左侧开始去遍历数组的,所以 i 下标左侧的区间一定是排好序的 0 和 1,而当 i 右侧的区域则是未排序的一段数据和排好的 2 。
这意味着当 i 与 left 交换一次数据之后,可以直接++,但是当 i 与 right 交换一次数据之后,得到的可能是0,所以 i 就不能++,必须重新进行一次判断。
void sortColors(vector<int>& nums) {
int left = -1,right = nums.size(),i = 0;
while(i < right)
{
if(nums[i] == 0)
swap(nums[++left],nums[i++]);
else if(nums[i] == 2)
swap(nums[--right],nums[i]);
else
i++;
}
}
二.快速排序
上述分治思想,貌似和快速排序的思想很相似,相对的,我们也可以用分治的思想来优化快排,如果快速排序的数组中有很多重复元素,甚至是全是重复元素, 那快速排序的效率就会及其低下。
因此,我们希望优化一下快速排序,那么就可以通过上述三指针的思想,确定数组中的一个基准元素key,一次排序后将数组分为三块,左边块全是小于key,中间块全是等于key,右边块全是大于key,这样就能保证相同的元素不会继续参与排序。
其中key的选择,我们采用在排序数组中随机选择的方式,具体优化代码如下:
void quickSort(vector<int>& nums,int begin,int end)
{
if(begin >= end) return;
int left = begin - 1,right = end + 1;
int key = nums[begin + (rand() % (end - begin + 1))];
int i = begin;
while(i < right)
{
if(key < nums[i])
swap(nums[--right],nums[i]);
else if(key > nums[i])
swap(nums[++left],nums[i++]);
else
i++;
}
mySort(nums,begin,left);
mySort(nums,right,end);
}
vector<int> sortArray(vector<int>& nums)
{
srand(time(NULL));
quickSort(nums,0,nums.size() - 1);
return nums;
}
三.交易逆序对的总数
来看题目(出自力扣):
题目很好理解,就是让我们统计一个数组中,前一个数比后一个数大的数对有多少个。
很显然,这道题可以直接通过两层for循环遍历数组来暴力求解,但是我们希望用时间复杂度更加优秀的解法,该怎么解这道题呢???
本题,我们可以采用分治 + 归并的思想来优化时间复杂度。
首先我们需要复习一下归并排序的原理:将一个数组分成两块,分别将两块子数组内的元素进行排序,最后再将两块数组合在一起整体排序,如此递归下去,就能得到一个时间复杂度为O(N*logN)的排序效率。
那么本题为什么可以使用归并排序呢???
我们用题目的示例来做解释,将数组分为如下两块,能够看出,一开始两个子数组还是乱序,现在我们来统计两个子数组中的逆序对,分别是(9,7)和(5,4),统计完之后,这两个子数组就可以分别完成内部排序。
有没有发现其中的奥秘,我们将子数组分别排序之后,并不会影响相较于两个子数组中的两个元素的相对位置,比如7和6的前后位置是相对不变的,此时再将两个子数组进行整体排序,因为归并排序将两个子数组合并时,是分别去比较两个数字的大小的,所以我们就可以在这时得到逆序对。
得此结论,我们就可以在归并排序的过程中,去统计逆序对的个数。
class Solution {
int arr[50001];
public:
int mergeSort(vector<int>& record, int left,int right)
{
if(left >= right) return 0;
int mid = (left + right) >> 1;
int ret = 0;
ret += mergeSort(record,left,mid);
ret += mergeSort(record,mid + 1,right);
int cur1 = left,cur2 = mid + 1;
int i = 0;
while(cur1 <= mid && cur2 <= right)
{
if(record[cur1] > record[cur2])
{
ret += mid - cur1 + 1;
arr[i++] = record[cur2++];
}
else
arr[i++] = record[cur1++];
}
while(cur1 <= mid)
arr[i++] = record[cur1++];
while(cur2 <= right)
arr[i++] = record[cur2++];
for(int j = left; j <= right;j++)
record[j] = arr[j - left];
return ret;
}
int reversePairs(vector<int>& record) {
return mergeSort(record,0,record.size() - 1);
}
};
由于快排和归并排序都表现出分治的思想,所以在分治算法中二者是常客。