在一个无序序列中找出第k个元素,对于k很小或者很大时可以采取特殊的方法,比如用堆排序来实现 。但是对于与序列长度N成正比的k来说,就不是一件容易的事了,可能最容易想到的就是先将无序序列排序再遍历即可找出第k个元素。由于任何基于比较的排序算法不可能用少于Θ(N lgN)次比较来实现将所有元素排序,所以采用排序的方法的时间复杂度是线性对数级别的。
我们可以借鉴快速排序中将序列划分的思想来实现平均情况下线性级别的算法,算法实现如下:
public class KthElement { private static void exch(Comparable[] a, int i, int j)
{
Comparable swap = a[i];
a[i] = a[j];
a[j] = swap;
} private static boolean less(Comparable a, Comparable b)
{
return a.compareTo(b) < 0;
} private static int partition(Comparable[] a, int lo, int hi)
{
int i = lo;
int j = hi + 1;
Comparable v = a[lo];
while(true)
{
while(less(a[++i], v)) if(i == hi) break;
while(less(v, a[--j]));
if(i >= j) break;
exch(a, i, j);
}
exch(a, lo, j);
return j;
} public static Comparable select(Comparable[] a, int k)
{
int lo = 0;
int hi = a.length - 1;
while(hi > lo)
{
int j = partition(a, lo, hi);
if(j == k) break;
else if(j > k) hi = j - 1;
else if(j < k) lo = j + 1;
}
return a[k];
}
public static void main(String[] args) {
Integer[] ints = {5, 3, 1, 4, 2};
int find = (int) select(ints, 2);
System.out.println(find);
} }
在select方法中,使用partition方法将序列划分。如果k = j,问题就已经解决了; 如果k < j,就继续切分左字数组(令 hi = j - 1);如果k > j,就继续切分右子数组(令lo = j + 1)。该循环保证了lo左边的元素都小于等于a[lo...hi], 而hi右边的元素都大于等于a[lo...hi],我们不断切分直到数组中只剩下第k个元素。为何这个的时间复杂度是线性级别的,证明很复杂,在此给出理想情况下的简易证明。假设每次切分都从中间切分,则所有的比较次数为(N + N/2 + N/4 + N/8 +...)直到找到k,很显然这个和小于2N。平均情况下的复杂度为Θ( 2N + 2Kln(N/K) + 2(N - K)ln(N/(N-K)) )。当K = N/2时,复杂度为Θ((2 + 2ln2)N)。