0、算法设计
考虑这样一种分治算法。对于任意给定的数v,假设S中的数被分成三组:比v小的数、与v相等的数(可能会有多个)以及比v大的数。分别记这三组数为SL、SV和SR。例如,如果集合S如下所示:
S:2∣36∣5∣21∣8∣13∣11∣20∣5∣4∣1
S:2|36|5|21|8|13|11|20|5|4|1
S:2∣36∣5∣21∣8∣13∣11∣20∣5∣4∣1
针对v=5,S被分为三组,分别为
SL:2∣4∣1S_L: 2|4|1SL:2∣4∣1
SV:5∣5S_V: 5|5SV:5∣5
SR:36∣21∣8∣13∣11∣20S_R: 36|21|8|13|11|20SR:36∣21∣8∣13∣11∣20
搜索范围立即缩小,转而在S的这三个子集中的某一个继续进行。如果我们想要寻找集合S的第8小元素,我们知道它一定是SR的第3小元素,因为|SL|+|Sv|=5,即selection(S,8)=selection(SR,3)selection(S,8)=selection(S_R,3)selection(S,8)=selection(SR,3)
更一般的,通过比较k与这些子集所含元素数之间的大小关系,我们可以很快确定所要寻找的元素在哪个子集中:
selection(S,k)={selection(SL,k)k≤∣SL∣v∣SL∣<k≤∣SL∣+∣SV∣selection(SL,k−∣SL∣−∣SV∣)k>∣SL∣+∣SV∣
selection(S,k)=\left\{
\begin{array}{}
selection(S_L,k)& & {k \leq |S_L|}\\
v & & {|S_L| < k \leq |S_L|+|S_V|}\\
selection(S_L,k-|S_L|-|S_V|) & & { k > |S_L|+|S_V|}\\
\end{array} \right.
selection(S,k)=⎩⎨⎧selection(SL,k)vselection(SL,k−∣SL∣−∣SV∣)k≤∣SL∣∣SL∣<k≤∣SL∣+∣SV∣k>∣SL∣+∣SV∣
1、算法的效率
如果每次选的v恰好是中项,则它的运行时间将满足
T(n)=T(n/2)+O(n)T(n)=T(n/2)+O(n)T(n)=T(n/2)+O(n)
即O(n)
但我们不可能每次选中的v正好是中项,我们可以采取一个简单的策略:从S中随机选取v
很显然,算法的运行时间依赖于我们随机选取的v。完全有可能出现这样的情况:由于持续的背运,我们挑选出来的v总是数组中最大的元素(或是最小元素)。这样的话,每次我们只能将待搜索的数组大小缩减1。对应的,在之前的例子里,我们可能首先选出的v=36,然后选出v=21,以此类推。这种最差情况将使得我们的选择问题算法必须要执行
n+(n−1)+(n−2)+...+n2=O(n2)n+(n-1)+(n-2)+...+\frac n2 = O(n^2)n+(n−1)+(n−2)+...+2n=O(n2)
次操作(当寻找中项时)。但是,这种情况出现的概率还是很低的。同样很少出现的还有最佳情形,即每次随机选取的v刚好能将数组一分为二,从而使算法的运行时间达到O(n)。在O(n)和O(n2)的范围内,算法的平均运行时间是多少呢?幸运的是,它与最佳情形下的运行时间很接近。
我们规定,当v落在它所在数组中的四分之一位置和四分之三位置之间,这样的v是好的。则一个随机选中的v较好的概率是50%,则在平均两次划分操作之后,待搜索数组的大小将最多缩减至原始大小的四分之三。
基于这样的引理:在掷硬币游戏之中,在得到一次正面之前,平均需要掷一个匀质硬币2次。
则我们可以得到算法的期望时间:
T(n)≤T(3n4)+O(n)T(n)\leq T( \frac {3n}4) + O(n) T(n)≤T(43n)+O(n)
由该递推式可得T(n)=O(n)
即算法的运行时间为O(n)
2、代码实现
int findKthLargest(vector<int>& nums, int k) {
srand (time(NULL));
vector<int> left, right;
int leftCount = 0, middleCount = 0, rightCount = 0;
int numSelected = nums[(rand() % nums.size())];
for(auto num : nums) {
if(num < numSelected) {
left.push_back(num);
leftCount++;
} else if(num == numSelected) {
middleCount++;
} else {
right.push_back(num);
rightCount++;
}
}
if(k <= rightCount) {
return findKthLargest(right, k);
} else if(k > rightCount && k <= rightCount + middleCount) {
return numSelected;
} else {
return findKthLargest(left, k - rightCount - middleCount);
}
}