此类问题,我们运用排序是无法完成的,我们学习的最快的排序时间复杂度为O(N*logN),那我们如何去求解呢,下面给出两种解法。
我们上篇文章讲到的荷兰国旗问题,来实现快排,我们这个题是不需要对左右边来进行排序的,我们发现通过荷兰国旗划分区间后。
当然我们还是要随机选取这个x,下面就只要小改一下随机快速排序的代码就可以了,下面是代码
#include
#include
#include
using namespace std;
//荷兰国旗问题
pair<int, int> getflag(vector<int>&arr, int L, int R)
{
if (L > R)
{
return { -1,-1 };
}
if (L == R) return { L,R };
int less = L - 1;
int more = R;
int idx =L;
//随机取数x=arr[m];
int m = rand() % (R - L + 1) + L;
//把它与arr[R]交换
swap(arr[m], arr[R]);
while (idx < more)
{
if (arr[idx] == arr[R]) idx++;
else if (arr[idx] < arr[R])
swap(arr[++less], arr[idx++]);
else if (arr[idx] > arr[R])
swap(arr[--more], arr[idx]);
}
swap(arr[R], arr[more]);
return { less + 1,more };
}
int process(vector<int>& arr, int L, int R,int idx)
{
if (L == R) return arr[L];
pair<int, int> tmp = getflag(arr, L, R);
//tmp为等于x的左右边界。
int left=tmp.first;
int right=tmp.second;
//如果已经找到
if(idx>=left&&idx<=right)
return arr[left];
//如果在左边区间
else if(idx<left)
return process(arr, L, left- 1,idx);
//如果在右边区间
else
return process(arr, right+ 1, R,idx);
}
这个算法是五个大牛的名字的总和,这个算法解决的也是这个荷兰国旗中的x如何选择,才可以保证时间复杂度尽可能的小,这个算法的时间复杂度是O(N)的。
这个算法的定义就是:找出一个无序数组中第k小的数
做法:首先把这个数组按照五个数一组,进行从小到大排序 。再取每个组的中位数(小组元素如果不足五个,奇数个数的同样取中位数,偶数个数取左中位数),把这些数在组成一组。然后再这一组中继续找它的中位数,这个过程可以直接递归套用算法的定义(也就是在新数组中找到第(l+r)/2小的数。)
最后我们就选出了我们的天选之子x,然后通过荷兰国旗问题区划分区间,为啥要这样去选x呢?我们下面来分析时间复杂度
为了方便理解,我们这里就取5的倍数个数的数组,这样就没有不足五个数的组了,这样分析好理解一些,时间复杂度只会少一些常数项,区别会很大,也没必要这么精确。
假设这个分好组的排序数组,圈中的是中位数组成的数组。
下面进行分析
所以我们找出的x,通过荷兰国旗问题划分区间后,左边的区间(小于x的区间)最少有3N/10的个数,右边的区间(大于x的区间)最多有7N/10个数,现在我们用最差情况来估算时间复杂度。
如果现在右区间有7N/10个数,我们的要找的数再右区间,我们在右区间找到这个数的时间复杂度为O(7N/10),最后我们加上排序分组的时间复杂度O(N)+O(N/5),时间复杂度公式T(N)=O(N)+O(N/5)+O(7*N/10),由于这是递归,且这个情况不可以用master公式,我们无法分析,但是数学证明了这个递归的时间复杂度为O(N),但是常数较大。
下面是master公式:
在计算涉及递归的算法的时候,计算复杂度就会变得有些麻烦。Master公式就是用来进行剖析递归行为和递归行为时间复杂度的估算的。
Master公式:T(N) = a*T(N/b) + O(Nd)公式解释:n表示问题的规模,a表示递归的次数也就是生成的子问题数,N/b表示子问题的规模。O(Nd)表示除了递归操作以外其余操作的复杂度结论(证明省略):
①当db a时,时间复杂度为O(Nlogb a)
②当d=logb a时,时间复杂度为O((Nd)*logN)
③当d>logb a时,时间复杂度为O(Nd)
注意:子问题规模必须等分,不管你是分成几部分
下面是代码
pair<int, int> partition(vector<int>& arr, int L, int R,int m)
{
if (L > R)
{
return { -1,-1 };
}
if (L == R) return { L,R };
int less = L - 1;
int more = R+1;
int idx = L;
//按照m去分区间
while (idx < more)
{
if (arr[idx] == m) idx++;
else if (arr[idx] < m)
swap(arr[++less], arr[idx++]);
else if (arr[idx] > m)
swap(arr[--more], arr[idx]);
}
return { less + 1,more-1};
}
//找出中间数的函数
int getMedion(vector<int>&tmp,int l,int r)
{
sort(tmp.begin() +l, tmp.begin()+r+1);
return tmp[(l+r)/2];
}
int bfprt(vector<int>& arr, int l, int r, int idx);
//找天选之子x的函数
int medianofMedians(vector<int>& arr, int l, int r)
{
int size = r - l + 1;
int offset = size % 5 == 0 ? 0 : 1;
vector<int> tmp(size/5 + offset);
for (int i = 0; i < tmp.size(); i++)
{
int teamfirst = l + i * 5;
tmp[i] = getMedion(arr, teamfirst, min(r, teamfirst + 4));
}
return bfprt(tmp, 0, tmp.size() - 1, tmp.size() / 2);
}
int bfprt(vector<int>&arr,int l,int r, int idx)
{
if (l == r)
{
return arr[l];
}
int m = medianofMedians(arr,l,r);
//荷兰国旗问题
pair<int, int>tmp= partition(arr, l, r, m);
if (idx >= tmp.first && idx <= tmp.second)
return arr[tmp.first];
else if(idx<tmp.first)
return bfprt(arr, l, tmp.first - 1, idx);
else
return bfprt(arr, tmp.second + 1, r, idx);
}
int minKth(vector<int>arr,int k)
{
//第k个数的下标因该是k-1。
return bfprt(arr, 0,arr.size()-1,k-1);
}
数组中第K大的元素
第k大的元素就是第n-k+1小的元素,直接套用bfprt就可以。
如果让你求出从小到大排序后,前k个的元素,我们把第k小的元素找出,然后遍历数组,比他小的元素放入数组中,最后如果还有空缺位置,说明和该元素相等,直接填上这个数就可以了。