前言
这个问题作用的数据集是这样的,假设有一个数组,其里面的每一个数据都不是重复的。然后在这样的个数组里,去寻找第k小的元素。这样的方法可以先排序,然后获取。这样的算法的时间复杂度为
Θ
(
n
l
g
n
)
\Theta(nlgn)
Θ(nlgn)。那能否在线性时间获取到第k小的元素呢?本文给出了两种方法来解决这个问题。
第一种方法其实是快速排序partition函数移植过来的,它的期望是
Θ
(
n
)
\Theta(n)
Θ(n),但是最坏的情况时,其时间复杂度为等差数列的和
Θ
(
n
2
)
\Theta(n^2)
Θ(n2);
第二种方法,针对方法一中的最坏的情况,提出了一种选择分割元素的方法,来解决这个问题,其时间复杂度为线性
Θ
(
c
n
)
\Theta(cn)
Θ(cn),由于这个c很大,所以这个算法应用并不是很广泛。
Selection in expected linear time
这里直接给出算法的伪代码:
Randomized-Select(A,p,r,i)
if p==r
return A[p]
q = Randomized-Partition(A,p,r)
k = q-p+1
if i == k
return A[q]
elseif i<k
return Randomized-Select(A,p,q-1,i)
else return Randomized-Select(A,q+1,r,i-k)
下面给出go实现的完整版的代码
func partition(A []int, left, right int) int{
flagNum := A[left]
partPosit := left
for i:=left+1; i<=right;i++{
if A[i] < flagNum {
partPosit++
A[partPosit], A[i] = A[i], A[partPosit]
}
}
A[partPosit], A[left] = A[left], A[partPosit]
return partPosit
}
func getRandNum(down, up int) int {
return int(rand.Int63n(int64(up-down)))+down
}
func RandomizedPartition(A []int, left, right int) int{
i:=getRandNum(left, right)
A[left], A[i] = A[i], A[left]
return partition(A, left, right)
}
func RandomizedSelect(A []int, left, right, i int) int {
if left == right {return A[left]}
flagNum := RandomizedPartition(A, left, right)
k := flagNum-left+1
if k == i{
return A[flagNum]
}else if k > i{
return RandomizedSelect(A, left, flagNum-1, i)
}else {
return RandomizedSelect(A, flagNum+1, right, i-k)
}
}
如上这是go语言实现的完整版的代码。下面对其时间复杂度进行分析:
证明我就不在这里证明了,其中用到了indicate random variable, 多项叠加,用cn的替换法但是这个的最坏的情况是怎样的呢?如果每次对长度为n的数组,分割的都是1,n-1这样的,那其时间复杂度会达到
n
2
n^2
n2。
Selection in worst-case linear time
考虑到上述算法,在最坏的情况下,其时间复杂度为
n
2
n^2
n2。在很久以前几位大佬提出了这样的一种分割方法。用代码实现起来还是很复杂的(不过本文会给出具体实现的代码)。
考虑到时间复杂度的不确定性,都是来自于分割位置的不确定性造成的。因此提出了这样的一种算法来选取分割元素。
首先将数组中的元素按照5个一组,划分为
[
n
/
5
]
[n/5]
[n/5],其中最后一组元素个数为
n
%
5
n\%5
n%5。
我们对每一组的5个元素选取中位数,也就是图中的白球,一共为
[
n
/
5
]
[n/5]
[n/5]个,然后再在这些元素中选择中位数。这样选择元素有什么好处呢?
我们在图上看到,通过划分获取的元素x,那么x在这些元素中所处的位置就是这样的。有一个象限的元素是大于x的,有一个象限的元素是小于x的。还有两个象限比较模糊。但是这个元素在数据中所处的位置,肯定不会是最糟糕的情况。然后我们就开始调用之前的算法解决问题。具体实现的代码如下(铁子,我觉得有点小复杂,如果有更合适的方法,可以告诉我):
var midNumMap map[int]int
var midNumArray []int
func RandomizedSelectForWorst(A []int, left, right, i int) int{
if left == right {return A[left]}
flagNum := RandomizedPartionForWorst(A, left, right)
k := flagNum-left+1
if k == i{
return A[flagNum]
}else if k > i{
return RandomizedSelect(A, left, flagNum-1, i)
}else {
return RandomizedSelect(A, flagNum+1, right, i-k)
}
}
func RandomizedPartionForWorst(A []int, left, right int) int {
i := SelectMidNum(A, left, right)
A[left], A[i] = A[i], A[left]
return partition(A, left, right)
}
func SelectMidNum(A []int, left, right int) int {
midNumMap = make(map[int]int)
midNumArray = make([]int, (right-left+1)/5)
fiveCnt := 0
for ; fiveCnt < (right-left+1)/5; fiveCnt++{
down := left+5*fiveCnt
insertSort(A, down, down+4)
midNumMap[A[down+2]] = down+2
midNumArray[fiveCnt] = A[down+2]
}
if num := (right-left+1)%5; num != 0{
down := right-num+1
insertSort(A, down, right)
midNumMap[A[(down+right)/2]] = (down+right)/2
midNumArray = append(midNumArray, A[(down+right)/2])
}
ithNum := 0
if len(midNumArray)%2 == 0{
ithNum = len(midNumArray)/2
}else {
ithNum = len(midNumArray)/2+1
}
midOfMids := RandomizedSelect(midNumArray,0, len(midNumArray)-1, ithNum)
return midNumMap[midOfMids]
}
func insertSort(A []int, left, right int){
len := right -left + 1
if len < 2 {
return
}
for i := left+1; i <= right; i++{
key := A[i]
j := i-1
for j >= left && A[j] > key{
A[j+1] = A[j]
j--
}
A[j+1] = key
}
}