本章我们要学习的是分治算法,顾名思义就是分而治之,把大问题分为多个相同的子问题进行处理,其中我们熟知的快速排序和归并排序用的就是分治算法,所以我们需要重新回顾一下这两个排序。
一、快速排序(三路划分)
1.算法思想:
首先从数列中确定一个需要排序的数(记为key),把所有key排到它的正确位置(即排序后它的位置),然后同样的方法,使用递归(分治思想)把小于key的区间与大于key的区间进行排序,直到这个区间不存在或只有一个元素时返回。
具体方法:从左到右遍历数组,并且我们使用三个变量(left,mid,right)把数组划分为四个区间,分别表示小于key,等于key,未遍历,大于key。而mid位置就是此时遍历到的数,判断这个数与key的关系,然后把它放在对应的区间。再用同样方法将大于key和小于key的区间排序。
注意:在此快速排序我使用了三路划分的优化方法,和对key取用随机值,这样比普通的快速排序效率要高得多,而且是一种很实用的分治算法。
2.代码实现:
vector<int> sortArray(vector<int>& nums)
{
srand((unsigned int)time(NULL));
dfs(0,nums.size()-1,nums);
return nums;
}
void dfs(int n,int m,vector<int>& nums)
{
if(n>=m) return;
int key=nums[n+rand()%(m-n+1)];
int left=n-1,right=m+1,mid=n;
while(mid<right)
{
if(nums[mid]<key) swap(nums[++left],nums[mid++]);
else if(nums[mid]==key) mid++;
else swap(nums[mid],nums[--right]);
}
dfs(n,left,nums);
dfs(right,m,nums);
}
二、归并排序
1.算法思想:
对于一个乱序的数组,我们可以把一分为二,先让两个小数组有序然后再将它们合并。那么如何让这两个小数组有序呢,同样的可以把它们分别再拆开,变成四个小组,然后继续一直往下拆,直到拆到只有一个元素,那么它必然是有序的,最后进行一一合并,这整个的思路有点像二叉树的后续遍历。
2.代码实现:
vector<int> tmp;
vector<int> sortArray(vector<int>& nums)
{
tmp.reserve(nums.size());
dfs(0,nums.size()-1,nums);
return nums;
}
void dfs(int left,int right,vector<int>& nums)
{
if(left>=right) return;
int mid=(left+right)>>1;
dfs(left,mid,nums);
dfs(mid+1,right,nums);
int p1=left,p2=mid+1;
int i=left;
while(p1<=mid&&p2<=right)
tmp[i++]=nums[p1]<=nums[p2]?nums[p1++]:nums[p2++];
while(p1<=mid) tmp[i++]=nums[p1++];
while(p2<=right) tmp[i++]=nums[p2++];
for(int j=left;j<=right;j++) nums[j]=tmp[j];
}
对于快速排序和归并排序,需要了解更多细节请参考下文:
【排序算法】—— 快速排序_快速排序方法-CSDN博客
【排序算法】—— 归并排序-CSDN博客
三、快速选择算法
当我们看到这个题目的时候,可能脑子里很快想到用堆解决TopK问题,或者是使用排序来解决。 但这是一个面试题,以上两种解决方法都不足以让我们拿到offer。需要注意题目描述是任意顺序返回这k个数,而不是有序返回,那么我们就应该意识到应该还有更优的解决方法。
对于使用堆和排序,时间复杂度都为O(nlogn),而这个题我们可以用快速选择算法,时间复杂度为O(n),这是由快速排序延伸出的一个算法。同样使用分治思想。
算法思想:
class Solution {
public:
vector<int> smallestK(vector<int>& arr, int k)
{
srand((unsigned int)time(NULL));
dfs(0,arr.size()-1,arr,k);
return {arr.begin(),arr.begin()+k};
}
void dfs(int l,int r,vector<int>& arr,int k)
{
if(l>r) return;
int key=arr[rand()%(r-l+1)+l];
int left=l-1,right=r+1,mid=l;
while(mid<right)
{
if(arr[mid]<key) swap(arr[++left],arr[mid++]);
else if(arr[mid]==key) mid++;
else swap(arr[mid],arr[--right]);
}
int a=left-l+1,b=mid-left-1;
if(k<a) dfs(l,left,arr,k);
else if(k<=a+b) return;
else dfs(right,r,arr,k-a-b);
}
};
四、逆序对问题
关于这个题,最容易想到的就是暴力方法,双重循环,时间复杂度为O(n^2),毫无疑问是通过不了这个题的,这个题我们可以利用归并排序来完成计数,时间复杂度为O(nlogn)。
算法思想:
class Solution {
public:
int count=0;
vector<int> tmp;
int reversePairs(vector<int>& record)
{
tmp.reserve(record.size());
dfs(0,record.size()-1,record);
return count;
}
void dfs(int left,int right,vector<int>& nums)
{
if(left>=right) return;
int mid=(left+right)>>1;
dfs(left,mid,nums);
dfs(mid+1,right,nums);
int p1=left,p2=mid+1;
int i=left;
while(p1<=mid&&p2<=right)
{
if(nums[p1]<=nums[p2]) tmp[i++]=nums[p1++];
else count+=mid-p1+1, tmp[i++]=nums[p2++];
}
while(p1<=mid) tmp[i++]=nums[p1++];
while(p2<=right) tmp[i++]=nums[p2++];
for(int i=left;i<=right;i++)
nums[i]=tmp[i];
}
};
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!