1. 问题定义
有
n
n
n个元素的数组
A
A
A,查找第
k
k
k小的元素,即将数组按从小到大排序,排在第
k
k
k位的元素。
注意,这里只是要找到这个元素,并不要求将数组排序。
首先,我们来看一下最朴素的求解问题的想法是什么?
第一步,数组
A
A
A按从小到大排序。
第二部,顺序找到
A
[
k
]
A[k]
A[k]并返回。
这个算法的算法时间复杂度:
排序的时间复杂度
+
找到第
k
个元素的时间复杂度
排序的时间复杂度+ 找到第k个元素的时间复杂度
排序的时间复杂度+找到第k个元素的时间复杂度
排序算法最优的时间复杂度已知是
O
(
n
l
g
n
)
O(nlgn)
O(nlgn)。
那么,顺序查找到第
k
k
k位的元素的时间复杂度是什么呢?
当
k
=
1
k=1
k=1,为最优时间
当
k
=
n
k=n
k=n,为最差时间为
当
k
=
⌈
n
+
1
2
⌉
或者
⌊
n
+
1
2
⌋
k=\lceil \frac{n+1}{2} \rceil 或者\lfloor \frac{n+1}{2} \rfloor
k=⌈2n+1⌉或者⌊2n+1⌋,为平均时间。
这里补充一点,不要把 A A A想象成数组,因为数组是连续地址空间的,可以通过A[x]来直接获取在x位置的元素。而应该把 A A A想成链表结构,每个元素的地址都存储是不连续的,每个元素里有一个地址指针指向下一个元素,因此获得某一元素地址必须从 A A A的第一个元素开始逐个往下查找。
在算法代码举例中,为了简化代码凸显算法,因此我们还是使用的数组结构。
那么有没有更优的算法呢?
2. 随机选择法
老规矩还是先奉上python源码,再举个例子来解释这个算法是如何运作的。
2.1 python源码
import random
def Rand_Partition(A,p,q):
x = random.randint(p,q-1)
A_head = []
A_tail = []
for a in A[p:q]:
if a < A[x]:
A_head.append(a)
elif a > A[x]:
A_tail.append(a)
return A[:p]+A_head+[A[x]]+A_tail+A[q:], len(A[:p]+A_head)
def Rand_Select(A,p,q,i):
if p==q:
return A[p]
A, r = Rand_Partition(A,p,q)
k = r-p+1
if i==k:
return A[r]
if i<k:
return Rand_Select(A,p,r-1,i)
else:
return Rand_Select(A,r+1,q,i-k)
2.2 代码分析
这个算法的主函数是Rand_Select, 且它是一个递归调用的函数。
Rand_Select有三个输入参数, A是一个无序的列表,是你要查找对象的列表。p是查找范围的起始位置,q是终止位置,i是你要查找元素在列表中按从小到大的排序序号,即查找第i小的元素。
Rand_Partition的作用是生成一个在p和q之间的随机数r,然后将A序列中小于这个A[r]的元素放到这个元素前面,而大于这个元素的元素放到它后面,然后返回这个随机数r和重新排列的A。
我们通过一个例子来更形象的理解它的运作过程。
数列A的初始状态如下图所示,我们要找值排在第7位的那个元素。
第一次进入时初始参数是Rand_Select(A, 0, 8, 7),再第一次Rand_Partition时选中了A[4]=8, 并且将小于8的元素全部移到它的左侧,大于8的元素则移到它右侧,此时返回8在A列表中的排位是4,小于我们要寻找的排位7,因此我们只需在8右侧的元素中进行递归寻找。
在进入到第一层递归时,我们查找的列表中只剩下3个元素,此时Rand_Partition随机选择了13这个元素,我们依旧是将比他大的移到左边,因为13是A列表中最大的元素,这会让13移到表尾。此时查看13的排位是8,大于7,则继续在13的左侧剩余元素中查找。
第二次进入递归时,表中元素仅剩10和11两个。如果此时随机选择算法选中了10,但因为这两个元素依旧按大小顺序排好了,因此这一轮中序列顺序不会改变。但是因为10的排序是6,所以继续进入第四层递归,且p=q=6,第四层递归直接返回了A[6]的元素。
这里补充一点,如果在第三层递归时,如果随机选中的元素是11的话,因为11的排序是7,正是我们要找的元素,那就不会进入第四层递归,而是直接返回该元素了。
3. 算法时间复杂度分析
在进行严谨的数学证明之前,请允许我们先直观的来想象一下在最佳和最坏的情况下,这个算法的时间复杂度是什么样的。
- 最佳情况,是我们随机选择的数字正好将序列划分成
1
10
\frac 1 {10}
101 和
9
10
\frac 9 {10}
109 ,此时算法的复杂度为:
T ( n ) < = T ( 9 10 n ) + θ ( n ) = θ ( n ) T(n)<= T(\frac 9{10}n) + \theta(n)=\theta(n) T(n)<=T(109n)+θ(n)=θ(n) - 最坏情况,是这个随机选择的数字是最大或最小值,将序列划分成
0
0
0和
n
−
1
n-1
n−1,此时算法复杂度为:
T ( n ) = T ( n − 1 ) + θ ( n ) = θ ( n 2 ) T(n) = T(n-1)+\theta(n)=\theta(n^2) T(n)=T(n−1)+θ(n)=θ(n2)
-接下来让我们来研究一下一般情况,或者称期望值。- 让 T ( n ) T(n) T(n)是随机选择算法作用于 n n n个元素列表运行时间的变量,假设随机数每次产生都是独立且随机的。
- 定义一个随机指标量
X k = { 1 如果产生的随机是是 k : n − k − 1 0 其他情况 X_k=\begin{cases} 1 &\text{如果产生的随机是是} k: n-k-1 \\ 0 &\text{其他情况 } \end{cases} Xk={10如果产生的随机是是k:n−k−1其他情况 - Rand_Partition对于
T
(
n
)
T(n)
T(n)的划分有
n
n
n种情况,可以表示为如下形式,而使用
X
k
X_k
Xk指标变量可以把这么多种情况巧妙的转换成和的形式。
T ( n ) = { T ( m a x { 0 , n − 1 } ) + θ ( n ) 如果0:n-1划分 T ( m a x { 1 , n − 2 } ) + θ ( n ) 如果1:n-2划分 . . . T ( m a x { n − 1 , 0 } ) + θ ( n ) 如果n-1:0划分 T(n)=\begin{cases} T(max \{0,n-1 \})+\theta(n) &\text{如果0:n-1划分} \\ T(max \{1,n-2 \})+\theta(n) &\text{如果1:n-2划分} \\ ...\\ T(max \{n-1,0 \})+\theta(n) &\text{如果n-1:0划分} \end{cases} T(n)=⎩ ⎨ ⎧T(max{0,n−1})+θ(n)T(max{1,n−2})+θ(n)...T(max{n−1,0})+θ(n)如果0:n-1划分如果1:n-2划分如果n-1:0划分
= ∑ k = 0 n − 1 X k ( T ( m a x { k , n − k − 1 } ) + θ ( n ) ) =\sum^{n-1}_{k=0} X_k(T(max \{k,n-k-1 \})+\theta(n)) =k=0∑n−1Xk(T(max{k,n−k−1})+θ(n))
有了这个式子我们可以来计算他的期望值:
E
[
T
(
n
)
]
=
E
[
∑
k
=
0
n
−
1
X
k
(
T
(
m
a
x
{
k
,
n
−
k
−
1
}
)
+
θ
(
n
)
)
]
E[T(n)]=E[\sum^{n-1}_{k=0} X_k(T(max \{k,n-k-1 \})+\theta(n))]
E[T(n)]=E[k=0∑n−1Xk(T(max{k,n−k−1})+θ(n))]
=
∑
k
=
0
n
−
1
E
[
X
k
(
T
(
m
a
x
{
k
,
n
−
k
−
1
}
)
+
θ
(
n
)
)
]
=\sum^{n-1}_{k=0} E[X_k(T(max \{k,n-k-1 \})+\theta(n))]
=∑k=0n−1E[Xk(T(max{k,n−k−1})+θ(n))]
=
∑
k
=
0
n
−
1
E
[
X
k
]
E
[
(
T
(
m
a
x
{
k
,
n
−
k
−
1
}
)
+
θ
(
n
)
)
]
=\sum^{n-1}_{k=0} E[X_k]E[(T(max \{k,n-k-1 \})+\theta(n))]
=∑k=0n−1E[Xk]E[(T(max{k,n−k−1})+θ(n))]
=
1
n
∑
k
=
0
n
−
1
E
[
(
T
(
m
a
x
{
k
,
n
−
k
−
1
}
)
]
+
1
n
∑
k
=
0
n
−
1
θ
(
n
)
=\frac1n\sum^{n-1}_{k=0} E[(T(max \{k,n-k-1 \})]+\frac1n\sum^{n-1}_{k=0}\theta(n)
=n1∑k=0n−1E[(T(max{k,n−k−1})]+n1∑k=0n−1θ(n)
<
=
2
n
∑
k
=
⌊
n
2
⌋
n
−
1
E
[
T
(
k
)
]
+
θ
(
n
)
<=\frac 2n \sum^{n-1}_{k=\lfloor \frac n2 \rfloor}E[T(k)]+\theta(n)
<=n2∑k=⌊2n⌋n−1E[T(k)]+θ(n)
下一步,我们使用替代法,假设在
c
c
c足够大的情况下
E
(
T
(
b
)
)
<
c
.
k
E(T(b))<c.k
E(T(b))<c.k,那么就有:
E
(
T
(
n
)
)
<
=
2
n
∑
k
=
⌊
n
2
⌋
n
−
1
E
[
T
(
k
)
]
+
θ
(
n
)
E(T(n))<=\frac 2n \sum^{n-1}_{k=\lfloor \frac n2 \rfloor}E[T(k)]+\theta(n)
E(T(n))<=n2∑k=⌊2n⌋n−1E[T(k)]+θ(n)
<
=
2
n
∑
k
=
⌊
n
2
⌋
n
−
1
c
.
k
+
θ
(
n
)
<= \frac 2n \sum^{n-1}_{k=\lfloor \frac n2 \rfloor} c.k +\theta(n)
<=n2∑k=⌊2n⌋n−1c.k+θ(n)
=
2
c
n
∑
k
=
⌊
n
2
⌋
n
−
1
k
+
θ
(
n
)
=\frac {2c}n \sum^{n-1}_{k=\lfloor \frac n2 \rfloor} k +\theta(n)
=n2c∑k=⌊2n⌋n−1k+θ(n)
其中
∑
k
=
⌊
n
2
⌋
n
−
1
k
<
3
8
n
2
\sum^{n-1}_{k=\lfloor \frac n2 \rfloor}k<\frac38n^2
∑k=⌊2n⌋n−1k<83n2,用等差数列求和公式即可得到,所以:
<
=
2
c
n
.
3
8
n
2
+
θ
(
n
)
<=\frac{2c}n . \frac38n^2+\theta(n)
<=n2c.83n2+θ(n)
=
3
4
c
n
+
θ
(
n
)
=\frac34 cn +\theta(n)
=43cn+θ(n)
=
c
n
−
(
1
4
c
n
−
θ
(
n
)
)
=cn-(\frac14cn-\theta(n))
=cn−(41cn−θ(n))
当c大于某一值时,我们可以另
1
4
c
n
−
θ
(
n
)
\frac14cn-\theta(n)
41cn−θ(n)非负,这样就证明了
E
(
T
(
n
)
)
<
c
.
k
,当
c
足够大时
E(T(n))<c.k,当c足够大时
E(T(n))<c.k,当c足够大时
结论,
E
(
T
(
n
)
)
E(T(n))
E(T(n))的期望值是
θ
(
n
)
\theta(n)
θ(n),即线性的,非常不错的性能表现。