十大算法之线性查找算法

BFPRT(线性查找算法)
BFPRT 算法解决的问题十分经典,即从某 n 个元素的序列中选出第 k 大(第 k 小)
的元素,通过巧妙的分析,BFPRT 可以保证在最坏情况下仍为线性时间复杂度。
该算法的思想与快速排序思想相似,当然,为使得算法在最坏情况下,依然能达

到 o(n)的时间复杂度,五位算法作者做了精妙的处理

算法步骤:
1. 将 n 个元素每5个一组,分成 n/5(上界)组。
2. 取出每一组的中位数,任意排序方法,比如插入排序。
3. 递归的调用 selection 算法查找上一步中所有中位数的中位数,设为 x,偶
数个中位数的情况下设定为选取中间小的一个。
4. 用 x 来分割数组,设小于等于 x 的个数为 k,大于 x 的个数即为 n-k。
5. 若 i==k,返回 x;若 i<k,在小于 x 的元素中递归查找第 i 小的元素;若 i>k,
在大于 x 的元素中递归查找第 i-k 小的元素。
终止条件:n=1时,返回的即是 i 小元素

寻找最小的 k 个数
题目描述
输入 n 个整数,输出其中最小的 k 个。
分析与解法
解法一
要求一个序列中最小的 k 个数,按照惯有的思维方式,则是先对这个序列从小到

大排序,然后输出前面的最小的 k 个数。
至于选取什么的排序方法,我想你可能会第一时间想到快速排序(我们知道,快
速排序平均所费时间为 n*logn ),然后再遍历序列中前 k 个元素输出即可。
因此,总的时间复杂度: O(n * log n)+O(k)=O(n * log n) 。

解法二
咱们再进一步想想,题目没有要求最小的 k 个数有序,也没要求最后 n-k 个数有
序。既然如此,就没有必要对所有元素进行排序。这时,咱们想到了用选择或交
换排序,即:
1、遍历 n 个数,把最先遍历到的 k 个数存入到大小为 k 的数组中,假设它们即
是最小的 k 个数;
2、对这 k 个数,利用选择或交换排序找到这 k 个元素中的最大值 kmax(找最大
值需要遍历这 k 个数,时间复杂度为 O(k) );
3、继续遍历剩余 n-k 个数。假设每一次遍历到的新的元素的值为 x,把 x 与 kmax
比较:如果 x < kmax ,用 x 替换 kmax,并回到第二步重新找出 k 个元素的
数组中最大元素 kmax‘;如果 x >= kmax ,则继续遍历不更新数组。
每次遍历,更新或不更新数组的所用的时间为 O(k) 或 O(0) 。故整趟
下来,时间复杂度为 n*O(k)=O(n*k)
解法三
更好的办法是维护容量为 k 的最大堆,原理跟解法二的方法相似:
 1、用容量为 k 的最大堆存储最先遍历到的 k 个数,同样假设它们即是最
小的 k 个数;
 2、堆中元素是有序的,令 k1<k2<...<kmax(kmax 设为最大堆中的最大元
素)
 3、遍历剩余 n-k 个数。假设每一次遍历到的新的元素的值为 x,把 x 与
堆顶元素 kmax 比较:如果 x < kmax ,用 x 替换 kmax,然后更新堆(用
时 logk);否则不更新堆。
这样下来,总的时间复杂度: O(k+(n-k)*logk)=O(n*logk) 。此方法得
益于堆中进行查找和更新的时间复杂度均为: O(logk) (若使用解法二:在数
组中找出最大元素,时间复杂度: O(k))
解法四
在《数据结构与算法分析--c 语言描述》一书,第7章第7.7.6节中,阐述了一种
在平均情况下,时间复杂度为 O(N) 的快速选择算法。如下述文字:
Since we can sort the file in O(nlog n) time, one might expect to obtain
a better time bound for selection. The algorithm we present to find the
kth smallest element in a set S is almost identical to quicksort. In fact,
the first three steps are the same. We will call this algorithm quickselect
(叫做快速选择). Let |Si| denote the number of elements in Si(令|Si|
为 Si 中元素的个数). The steps of quickselect are( 步骤如下 ):
 If |S| = 1, then k = 1 and return the elements in S as the answer.
If a cutoff for small files is being used and |S| <=CUTOFF, then sort
S and return the kth smallest element.
 Pick a pivot element, v (- S.(选取 S 中一个元素作为枢纽元 v)
 Partition S - {v} into S1 and S2, as was done with quicksort. (将
集合 S-{v}分割成 S1和 S2,就像快速排序那样)
 If k <= |S1|, then the kth smallest element must be in S1. In this
case, return quickselect (S1, k). If k = 1 + |S1|, then the pivot is
the kth smallest element and we can return it as the answer. Otherwise,
the kth smallest element lies in S2, and it is the (k - |S1| - 1)st
smallest element in S2. We make a recursive call and return quickselect
(S2, k - |S1| - 1). (如果 k<=|S1|,那么第 k 个最小元素必然在 S1中。
在这种情况下,返回 quickselect(S1,k)。如果 k=1+|S1|,那么枢纽元素就
是第 k 个最小元素,即找到,直接返回它。否则,这第 k 个最小元素就在 S2
中,即 S2中的第(k-|S1|-1)个最小元素,我们递归调用并返回 quickselect
(S2,k-|S1|-1))。
In contrast to quicksort, quickselect makes only one recursive call
instead of two. The worst case of quickselect is identical to that of
quicksort and is O(n2). Intuitively, this is because quicksort's worst
case is when one of S1 and S2 is empty; thus, quickselect(快速选择) is
not really saving a recursive call. The average running time, however,
is O(n)( 不过,其平均运行时间为 O(N) ). The analysis is similar to
quicksort's and is left as an exercise.

示例代码

//QuickSelect 将第 k 小的元素放在 a[k-1]
void QuickSelect( int a[], int k, int left, int right )
{
int i, j;
int pivot;
if( left + cutoff <= right )
{
pivot = median3( a, left, right );
// 取三数中值作为枢纽元,可以很大程度上避免最坏情况
i = left; j = right - 1;
for( ; ; )

{
while( a[ ++i ] < pivot ){ }
while( a[ --j ] > pivot ){ }
if( i < j )
swap( &a[ i ], &a[ j ] );
else
break;
}
// 重置枢纽元
swap( &a[ i ], &a[ right - 1 ] );
if( k <= i )
QuickSelect( a, k, left, i - 1 );
else if( k > i + 1 )
QuickSelect( a, k, i + 1, right );
}
else
InsertSort( a + left, right - left + 1 );
}

这个快速选择 SELECT 算法,类似快速排序的划分方法。N 个数存储在数组 S 中,
再从数组中选取“中位数的中位数”作为枢纽元 X,把数组划分为 Sa 和 Sb 俩部
分,Sa<=X<=Sb,如果要查找的 k 个元素小于 Sa 的元素个数,则返回 Sa 中较小
的 k 个元素,否则返回 Sa 中所有元素+Sb 中小的 k-|Sa|个元素,这种解法在平
均情况下能做到 O(N) 的复杂度

解法五
《算法导论》介绍了一个随机选取主元的选择算法 RANDOMIZED-SELECT。它每次
都是随机选取数列中的一个元素作为主元,在 O(n) 的时间内找到第 k 小的
元素,然后遍历输出前面的 k 个小的元素。平均时间复杂度: O(n+k)=O(n)
(当 k 比较小时)。
我们知道,快速排序是以固定的第一个或最后一个元素作为主元,每次递归划分
都是不均等的,最后的平均时间复杂度为: O(n*logn)。而 RANDOMIZED-SELECT
与普通的快速排序不同,它每次递归都是随机选择序列,从第一个到最后一个元
素中任一一个作为主元。
下面是 RANDOMIZED-SELECT(A, p, r)完整伪码

PARTITION(A, p, r)
//partition 过程 p 为第一个数, r 为最后一个

1 x ← A[r]
// 以最后一个元素作为主元
2 i ← p - 1
3 for j ← p to r - 1
4 do if A[j] ≤ x
5 then i ← i + 1

6 exchange A[i] <-> A[j]
7 exchange A[i + 1] <-> A[r]
8 return i + 1
RANDOMIZED-PARTITION(A, p, r)
// 随机快排的 partition 过程
1 i ← RANDOM(p, r)
//i 随机取 p 到 r
中个一个值
2 exchange A[r] <-> A[i]
// 以随机的 i 作为主 元
3 return PARTITION(A, p, r)
// 调用上述原来的 partition 过程
RANDOMIZED-SELECT(A, p, r, i)
// 以线性时间做选择,目的是返回数 组
A[p..r] 中的第 i 小的元素
1 if p = r
//p=r ,序列中只有一个元素
2 then return A[p]
3 q ← RANDOMIZED-PARTITION(A, p, r)
// 随机选取的元素 q 作为主元
4 k ← q - p + 1
//k 表示子数组 A[p … q] 内的元素个
数,处于划分低区的元素个数加上一个主元元素
5 if i == k
// 检查要查找的 i 等于子数组 中
A[p....q] 中的元素个数 k
6 then return A[q]
// 则直接返回 A[q]
7 else if i < k

8 then return RANDOMIZED-SELECT(A, p, q - 1, i)
// 得到的 k 大于要查找的 i 的大小,则递归到低区间 A[p , q-1] 中
去查找
9 else return RANDOMIZED-SELECT(A, q + 1, r, i - k)
// 得到的 k 小于要查找的 i 的大小,则递归到高区间 A[q+1 , r] 中
去查找。
下面则是《算法导论》原版关于 RANDOMIZED-SELECT(A, p, r)为 O(n) 的
证明,阐述如下:
此 RANDOMIZED-SELECT 最坏情况下时间复杂度为Θ(n2),即使是要选择最小元素
也是如此,因为在划分时可能极不走运,总是按余下元素中的最大元素进行划分,
而划分操作需要 O(n)的时间。
然而此算法的平均情况性能极好,因为它是随机化的,故没有哪一种特别的输入
会导致其最坏情况的发生。
算法导论上,针对此
RANDOMIZED-SELECT 算法平均时间复杂度为 O( n )的证明 ,
引用如下,或许,能给你我多点的启示(本来想直接引用第二版中文版的翻译文
字,但在中英文对照阅读的情况下,发现第二版中文版的翻译实在不怎么样,所
以,得自己一个一个字的敲,最终敲完修正如下),分4步证明:
当 RANDOMIZED-SELECT 作用于一个含有 n 个元素的输入数组 A[p ..r]上时,所
需时间是一个随机变量,记为 T(n),我们可以这样得到线性期望值 E [T(n)]的下
界:程序 RANDOMIZED-PARTITION 会以等同的可能性返回数组中任何一个元素为
主元,因此,对于每一个 k,(1 ≤k ≤n),子数组 A[p ..q]有 k 个元素,它们
全部小于或等于主元元素的概率为1/n.对 k = 1, 2,...,n,我们定指示器 X k ,
为:
X k = I{子数组 A[p ..q]恰有 k 个元素} ,

我们假定元素的值不同,因此有
E[X k ]=1/n
当调用RANDOMIZED-SELECT并且选择 A[q]作为主元元素的时候,我们事先不知道
是否会立即找到我们所想要的第 i 小的元素,因为,我们很有可能需要在子数组
A[p ..q - 1], 或 A[q + 1 ..r]上递归继续进行寻找.具体在哪一个子数组上递
归寻找,视第 i 小的元素与 A[q]的相对位置而定.
假设 T(n)是单调递增的,我们可以将递归所需时间的界限限定在输入数组时可
能输入的所需递归调用的最大时间(此句话,原中文版的翻译也是有问题的).
换言之,我们断定,为得到一个上界,我们假定第 i 小的元素总是在划分的较大的
一边,对一个给定的 RANDOMIZED-SELECT,指示器 Xk 刚好在一个 k 值上取1,在
其它的 k 值时,都是取0.当 Xk =1时,可能要递归处理的俩个子数组的大小分别
为 k-1,和 n-k,因此可得到递归式为

http://taop.marchtea.com/images/2/2.1/2.1.8.jpg

取期望值为

http://taop.marchtea.com/images/2/2.1/2.1.9.jpg

为了能应用等式
(C.23)
,我们依赖于 X k 和 T(max(k - 1,n - k))是独立的随
机变量(这个可以证明,证明此处略)。
3. 下面,我们来考虑下表达式 max(k - 1,n -k)的结果.我们有

http://taop.marchtea.com/images/2/2.1/2.1.10.jpg

如果 n 是偶数,从 T(⌉ )到 T(n - 1)每个项在总和中刚好出现俩次,T(⌋ )出现
一次。因此,有

http://taop.marchtea.com/images/2/2.1/2.1.11.jpg

我们可以用替换法来解上面的递归式。假设对满足这个递归式初始条件的某个常
数 c,有 T(n) ≤cn。我们假设对于小于某个常数 c(稍后再来说明如何选取这
个常数)的 n,有 T(n) =O(1)。 同时,还要选择一个常数 a,使得对于所有的
n>0,由上式中 O(n)项(用来描述这个算法的运行时间中非递归的部分)所描述的
函数,可由 an 从上方限界得到(这里,原中文版的翻译的确是有点含糊)。利用
这个归纳假设,可以得到

http://taop.marchtea.com/images/2/2.1/2.1.12.jpg

 为了完成证明,还需要证明对足够大的 n,上面最后一个表达式最大为 cn,
即要证明:cn/4 -c/2 -an ≥ 0.如果在俩边加上 c/2,并且提取因子 n,就
可以得到 n(c/4 -a) ≥c/2.只要我们选择的常数 c 能满足 c/4 -a > 0, i.e.,
即 c > 4a,我们就可以将俩边同时除以 c/4 -a, 最终得到

http://taop.marchtea.com/images/2/2.1/2.1.13.jpg

综上,如果假设对 n < 2c/(c -4a),有 T(n) =O(1),我们就能得到 E[T(n)] =O(n)。
所以,最终我们可以得出这样的结论,并确认无疑: 在平均情况下,任何顺序
统计量(特别是中位数)都可以在线性时间内得到 。
结论:RANDOMIZED-SELECT 的时间复杂度为 O(N) ,但它在最坏情况下时间
的复杂度为 O(N^2)

解法五
《算法导论》第九章第9.3节介绍了一个最坏情况线性时间的选择算法,如下:
9.3 Selection in worst-case linear time(
最坏情况下线性时间的选择算法

We now examine a selection algorithm whose running time isO(n) in the worst
case(现在来看,一个最坏情况运行时间为 O(N)的选择算法 SELECT). Like
RANDOMIZED-SELECT, the algorithm SELECT finds the desired element by
recursively partitioning the input array. The idea behind the algorithm,
however, is toguarantee a good split when the array is partitioned. SELECT
uses the deterministic partitioning algorithm PARTITION from quicksort
(seeSection 7.1), modified to take the element to partition around as an
input parameter(像 RANDOMIZED-SELECT 一样,SELECTT 通过输入数组的递归
划分来找出所求元素,但是,该算法的基本思想是要保证对数组的划分是个好的
划分。SECLECT 采用了取自快速排序的确定性划分算法 partition,并做了修改,
把划分主元元素作为其参数).
The SELECT algorithm determines theith smallest of an input array ofn >
1 elements by executing the following steps. (Ifn = 1, then SELECT merely
returns its only input value as theith smallest.)(算法 SELECT 通过执行
下列步骤来确定一个有 n>1个元素的输入数组中的第 i 小的元素。 (如果 n=1,则
SELECT 返回它的唯一输入数值作为第 i 个最小值。))
 Divide then elements of the input array into⌋ groups of 5 elements
each and at most one group made up of the remainingn mod 5 elements.

 Find the median of each of the⌉ groups by first insertion sorting
the elements of each group (of which there are at most 5) and then
picking the median from the sorted list of group elements.  Use SELECT recursively to find the medianx of the⌉ medians found
in step 2. (If there are an even number of medians, then by our
convention,x is the lower median.)
 Partition the input array around the median-of-mediansx using the
modified version of PARTITION. Letk be one more than the number of
elements on the low side of the partition, so thatx is thekth smallest
element and there aren-k elements on the high side of the partition.
(利用修改过的 partition 过程,按中位数的中位数 x 对输入数组进行划分,
让 k 比划低去的元素数目多1,所以,x 是第 k 小的元素,并且有 n-k 个元素
在划分的高区)
 Ifi =k, then returnx. Otherwise, use SELECT recursively to find
theith smallest element on the low side ifi k.( 如果要找的第 i 小的
元素等于程序返回的 k ,即 i=k,则返回 x。否则,如果 ik,则在高区间找
第(i-k)个最小元素)

http://taop.marchtea.com/images/2/2.1/2.1.20.jpg

(以上五个步骤,即本文上面的第四节末中所提到的所谓“五分化中项的中项”

的方法。)
To analyze the running time of SELECT, we first determine a lower bound
on the number of elements that are greater than the partitioning element
x. (为了分析 SELECT 的运行时间,先来确定大于划分主元元素 x 的的元素数的
一个下界)Figure 9.1 is helpful in visualizing this bookkeeping. At least
half of the medians found in step 2 are greater than[1] the median-of-medians x. Thus, at least half of the ⌉ groupscontribute 3
elements that are greater than x, except for the one group that has fewer
than 5 elements if 5 does not dividen exactly, and the one group
containingx itself. Discounting these two groups, it follows that the
number of elements greater thanx is at least

http://taop.marchtea.com/images/2/2.1/2.1.22.jpg

(Figure 9.1: 对上图的解释或称对 SELECT 算法的分析:n 个元素由小圆圈来
表示,并且每一个组占一纵列。组的中位数用白色表示,而各中位数的中位数 x
也被标出。 (当寻找偶数数目元素的中位数时,使用下中位数)。箭头从比较大的
元素指向较小的元素,从中可以看出,在 x 的右边,每一个包含5个元素的组中
都有3个元素大于 x,在 x 的左边,每一个包含5个元素的组中有3个元素小于 x。
大于 x 的元素以阴影背景表示。 )
Similarly, the number of elements that are less thanx is at least 3n/10

- 6. Thus, in the worst case, SELECT is called recursively on at most 7n/10
+ 6 elements in step 5.
We can now develop a recurrence for the worst-case running timeT(n) of
the algorithm SELECT. Steps 1, 2, and 4 take O(n) time. (Step 2 consists ofO(n) calls of insertion sort on sets of sizeO(1).) Step 3 takes timeT(⌉ ),
and step 5 takes time at mostT(7n/10+ 6), assuming thatT is monotonically
increasing. We make the assumption, which seems unmotivated at first, that
any input of 140 or fewer elements requiresO(1) time; the origin of the
magic constant 140 will be clear shortly. We can therefore obtain the
recurrence

http://taop.marchtea.com/images/2/2.1/2.1.23.jpg

We show that the running time is linear by substitution. More specifically,
we will show thatT(n) ≤cn for some suitably large constant c and alln
> 0. We begin by assuming thatT(n) ≤cn for some suitably large constantc
and alln ≤ 140; this assumption holds ifc is large enough. We also pick
a constanta such that the function described by theO(n) term above (which
describes the non-recursive component of the running time of the algorithm)
is bounded above byan for alln > 0. Substituting this inductive hypothesis
into the right-hand side of the recurrence yields
T(n) ≤ c⌉ +c(7n/10 + 6) +an
≤ cn/5 +c + 7cn/10 + 6c +an
= 9cn/10 + 7c +an
= cn + (-cn/10 + 7c +an)
which is at mostcn if

http://taop.marchtea.com/images/2/2.1/2.1.24.jpg

Inequality (9.2) is equivalent to the inequalityc ≥ 10a(n/(n - 70)) when
n > 70. Because we assume thatn ≥ 140, we have n/(n - 70) ≤ 2, and so
choosing c ≥ 20a will satisfyinequality (9.2). (Note that there is
nothing special about the constant 140; we could replace it by any integer
strictly greater than 70 and then choosec accordingly.) The worst-case
running time of SELECT is therefore linear( 因此,此 SELECT 的最坏情况
的运行时间是线性的 ).
As in a comparison sort (seeSection 8.1), SELECT and RANDOMIZED-SELECT
determine information about the relative order of elements only by
comparing elements. Recall fromChapter 8 that sorting requiresΩ(n lgn)
time in the comparison model, even on average (see Problem 8-1). The
linear-time sorting algorithms in Chapter 8 make assumptions about the
input. In contrast, the linear-time selection algorithms in this chapter
do not require any assumptions about the input. They are not subject to
the Ω(n lgn) lower bound because they manage to solve the selection
problem without sorting.
(与比较排序(算法导论8.1节)中的一样,SELECT 和 RANDOMIZED-SELECT 仅通
过元素间的比较来确定它们之间的相对次序。在算法导论第8章中,我们知道在
比较模型中,即使在平均情况下,排序仍然要 O(n*logn) 的时间。第8章得
线性时间排序算法在输入上做了假设。 相反地,本节提到的此类似 partition
过程的 SELECT 算法不需要关于输入的任何假设,它们不受下界 O(n*logn) 的
约束,因为它们没有使用排序就解决了选择问题 (看到了没,道出了此算法的
本质阿))
Thus, the running time is linear because these algorithms do not sort;
the linear-time behavior is not a result of assumptions about the input,
as was the case for the sorting algorithms inChapter 8. Sorting requires
Ω(n lgn) time in the comparison model, even on average (see Problem 8-1),
and thus the method of sorting and indexing presented in the introduction

to this chapter is asymptotically inefficient.(所以,本节中的选择算法
之所以具有线性运行时间,是因为这些算法没有进行排序;线性时间的结论并不
需要在输入上所任何假设,即可得到)

举一反三
1、谷歌面试题:输入是两个整数数组,他们任意两个数的和又可以组成一个数
组,求这个和中前 k 个数怎么做?
分析:
“假设两个整数数组为 A 和 B,各有 N 个元素,任意两个数的和组成的数组 C 有
N^2个元素。
那么可以把这些和看成 N 个有序数列:
A[1]+B[1] <= A[1]+B[2] <= A[1]+B[3] <=…
A[2]+B[1] <= A[2]+B[2] <= A[2]+B[3] <=… …
A[N]+B[1] <= A[N]+B[2] <= A[N]+B[3] <=…
问题转变成,在这 N^2个有序数列里,找到前 k 小的元素”
2、有两个序列 A 和 B,A=(a1,a2,...,ak),B=(b1,b2,...,bk),A 和 B 都按升序排
列。对于1<=i,j<=k,求 k 个最小的(ai+bj)。要求算法尽量高效。
3、给定一个数列 a1,a2,a3,...,an 和 m 个三元组表示的查询,对于每个查询(i,
j,k),输出 ai,ai+1,...,aj 的升序排列中第 k 个数。

寻找最小(最大)的 k 个数
题目描述:输入 n 个整数,输出其中最小的 k 个元素。
例如:输入1,2,3,4,5,6,7,8这8个数字,则最小的4个数字为1,2,3,4。

思路1:最容易想到的方法:先对这个序列从小到大排序,然后输出前面的最小
的 k 个数即可。如果选择快速排序法来进行排序,则时间复杂度:O(n*logn)
思路2:在思路1的基础上更进一步想想,题目并没有要求要查找的 k 个数,甚至
后 n-k 个数是有序的,既然如此,咱们又何必对所有的 n 个数都进行排序列?如
此,我们能想打的一个方法是:遍历 n 个数,先把最先遍历到得 k 个数存入大小
为 k 的数组之中,对这 k 个数,利用选择或交换排序,找到 k 个数中的最大数
kmax(kmax 设为 k 个元素的数组中最大元素),用时 O(k)(你应该知道,插入
或选择排序查找操作需要 O(k)的时间),后再继续遍历后 n-k 个数,x 与 kmax
比较:如果 x<kmax,则 x 代替 kmax,并再次重新找出 k 个元素的数组中最大元
素 kmax‘;如果 x>kmax,则不更新数组。这样,每次更新或不更新数组的所用
的时间为 O(k)或 O(0),整趟下来,总的时间复杂度平均下来为:n*O(k)=O
(n*k)
思路3:与思路2方法类似,只是用容量为 k 的最大堆取代思路2中数组的作用(从
数组中找最大数需要 O(k)次查找,而从更新一个堆使之成为最大堆只需要
O(logk)次操作)。具体做法如下:用容量为 k 的最大堆存储最先遍历到的 k 个数,
并假设它们即是最小的 k 个数,建堆费时 O(k)后,有 k1<k2<…<kmax(kmax
设为大顶堆中最大元素)。继续遍历数列,每次遍历一个元素 x,与堆顶元素比
较,x<kmax,更新堆(用时 logk),否则不更新堆。这样下来,总费时 O(k+(n-k)
*logk)=O(n*logk)。
思路4:按编程之美中给出的描述,类似快速排序的划分方法,N 个数存储在数
组 S 中,再从数组中随机选取一个数 X(随机选取枢纽元,可做到线性期望时间
O(N)的复杂度),把数组划分为 Sa 和 Sb 俩部分,Sa<=X<=Sb,如果要查找的 k
个元素小于 Sa 的元素个数,则返回 Sa 中较小的 k 个元素,否则返回 Sa 中所有
元素+Sb 中小的 k-|Sa|个元素。像上述过程一样,这个运用类似快速排序的
partition 的快速选择 SELECT 算法寻找最小的 k 个元素,在最坏情况下亦能做
到 O(N)的复杂度。oh,太酷了,有没有!
思路5:仍然用到数据结构:堆。具体做法为:针对整个数组序列建最小堆,建
堆所用时间为O(n),然后取堆中的前k 个数,总的时间复杂度即为: O (n+k*logn)。

思路6:与上述思路5类似,不同的是在对元素数组原地建最小堆 O(n)后,然后
提取 K 次,但是每次提取时,换到顶部的元素只需要下移顶多 k 次就足够了,下
移次数逐次减少( 而上述思路 5 每次提取都需要 logn ,所以提取 k 次,思路 7 需
要 k*logn 。 而本思路只需要 K^2 )。此种方法的复杂度为 O(n+k^2)。此方法可能
不太直观,一个更直观的理解是:每次取出堆顶元素后,最小堆的性质被破坏了,
我们需要调整最小堆使之满足最小堆的性质。由于我们只需要求取前 k 个数,我
们无需将整个堆都完整的调整好,只需保证堆的最上面 k 个数是最小的就可以,
即第一趟调整保持第0层到第 k 层是最小堆,第二趟调整保持第0层到第 k-1层是
最小堆…,依次类推。
在编码实现上述思路之前,你可能需要了解:快速排序、堆排序

思路3的一个实现:

#include <stdio.h>

#include <stdio.h>

#include <stdlib.h>

#define PARENT(i) (i)/2

#define LEFT(i) 2*(i)+1

#define RIGHT(i) 2*(i+1)
void swap(int *a,int *b)

{

*a=*a^*b; *b=*a^*b; *a=*a^*b

; }

void max_heapify(int *arr,int index,int len)

{

int l=LEFT(index);

int r=RIGHT(index);

int largest;

if(l<len && arr[l]>arr[index])

largest=l; else largest=index;

if(r<len && arr[r]>arr[largest])

largest=r;

if(largest != index){//将最大元素提升,并递归

swap(&arr[largest],&arr[index]);

max_heapify(arr,largest,len); }
}
void build_maxheap(int *arr,int len)

{

int i;

if(arr==NULL || len<=1)

return;

for(i=len/2+1;i>=0;--i)

max_heapify(arr,i,len);

}
void k_min(int *arr,int len,int k)

{

int i;

build_maxheap(arr,k);

for (i = k;i < len;i++)

{

if (arr[i] < arr[0])

{

arr[0] = arr[i];

max_heapify(arr,0,k);

}

}

}

/* void heap_sort(int *arr,int len)

{

int i;

if(arr==NULL || len<=1)

return;

build_maxheap(arr,len);

}
} */

int main()

{

int arr[10]={91,8,6,82,15,18,7,46,29,12};

int i;

k_min(arr,10,4); for(i=0;i<10;++i)

printf("%d ",arr[i]); system("pause");


}
for(i=len-1;i>=1;--i){ swap(&arr[0],&arr[i]); max_heapify(arr,0,--len); }
}

思路4的实现

Kbig(S, k):

if(k <= 0):

return [] // 返回空数组

if(length S <= k):

return S

(Sa, Sb) = Partition(S)

return Kbig(Sa, k).Append(Kbig(Sb, k – length Sa)
Partition(S):

Sa = [] // 初始化为空数组

Sb = [] // 初始化为空数组

Swap(s[1], S[Random()%length S]) // 随机选择一个数作为分 组标准,以 // 避免特殊数据下的算法退化,也可 // 以通过对整个数据进行洗牌预处理 // 实现这个目的

p = S[1]

for i in [2: length S]:

S[i] > p ? Sa.Append(S[i]) : Sb.Append(S[i]) // 将 p 加入较小的组,可以避免分组失 败,也使分组 // 更均匀,提高效率

length Sa < length Sb ? Sa.Append(p) :

Sb.Append(p) return (Sa, Sb)
一个简化实现的如下:

#include <stdio.h>

#include <stdlib.h>
void swap(int *a,int *b)

{

*a=*a^*b; *b=*a^*b; *a=*a^*b;

} /* 为了简单起见,这里只是单纯的选取第一个元素作为枢纽元素。这样 选取枢纽,就难避免使得算法容易退化。 */

int k_big(int arr[],int low,int high,int k)

{

int pivot = arr[low];

int high_tmp = high;

int low_tmp = low; while(low < high){ //从右向左查找,直到找到第一个小于枢纽元素为止

while (low < high && arr[high] >= pivot)

{

--high;

}

arr[low] = arr[high]; //从左向右查找,直到找到第一个大于枢纽元素为止

while (low < high && arr[low] <= pivot)

{

++low;

} arr[high] = arr[low];

}

arr[low] = pivot;
if (low == k - 1)

{

return arr[low];

}

else if(low > k - 1)

{

return k_big(arr,low_tmp,low-1,k);

}

else {
return k_big(arr,low+1,high_tmp,k);
}
}

int main()

{

int arr[10]={-91,0,6,82,15,18,7,46,-29,12};

int i;

k_big(arr,0,9,4);

for(i=0;i<10;++i)

printf("%d ",arr[i]); system("pause");

}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值