06算法-查找第k小的数(order statistic)

前言

这个问题作用的数据集是这样的,假设有一个数组,其里面的每一个数据都不是重复的。然后在这样的个数组里,去寻找第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
	}
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值