问题很简单, n个元素list[0], list[1]... list[n-1], 求其第k小(或大)的元素 (注: 为了简单, 程序中是按照0-index. 也就是说第0小的是最小的,以此类推)
最简单的想法, 给这些元素排个序, 自然就出来了. 当然这样弄效率不是很高, 因为计算了许多额外的信息.
使用Hoare算法, 可以比较好的解决这个问题.
基本思路和快排类似. 分为以下几个步骤:
1. 分块. 目的是把队列中所有的元素, 小于某个值(p) 的元素放到 p 左面, 大于p的放到 p 右面, 最后返回p所在的索引. 比如, 队列为
1 3 2 4 6 7 选择的元素为a[1] = 3. 那么,分块后,队列为 1 2 3 6 4 7 返回3的位置2. (这个很熟悉吧?和快排的基本一样)
public static <E extends Comparable<? super E>> int partition(List<E> list, int left, int right, int pivotIndex) {
debug ( "partion :" + pivotIndex + " left:" + left + " right:" + right);
debug (list);
E pivotValue = list.get(pivotIndex);
swap (list, pivotIndex, right);
int storeIndex = left;
for ( int i=left; i<=right-1; i++) {
if (list.get(i).compareTo(pivotValue) < 0) {
swap (list, storeIndex, i);
storeIndex = storeIndex + 1;
}
}
swap (list, right, storeIndex);
debug (list);
//System.out.println("partion finished:" + storeIndex);
return storeIndex;
}
2. 分块后, 就可以进行判断.
设经过第1步分块后, 得到的索引为index.
如果, index == k , 那么问题解决;
如 果, index > k , 则说明 待求的第 k 个元素还在 队列的左边. 则对队列index左边的元素进行处理. 如下例, P 是 选择的元素, a0-a4 都比 P 小, b0-b4都比P大. 此时P的位置是 5. 若要求的第 k 小, 并且 k < 5 , 那么显然第k小的元素是坐落在a0~a4中. 在程序中, 是队列left到pivotNewIndex - 1 的位置;
a0 a1 a2 a3 a4 P b0 b1 b2 b3 b4
如果, index < k , 则说明 待求的第 k 个元素还在 队列的右边. 则对队列index右边的元素进行分块,重复这些步骤(上图中b0~b4的部分).
程序如下:
public static <E extends Comparable<? super E>> E select(List<E> list, int k, int left, int right) {
//随便选择一个作为P. 这里用的中点
int pivotIndex = (left + right) / 2;
int pivotNewIndex = partition (list, left, right, pivotIndex);
if (k == pivotNewIndex) {
return list.get(k);
}
if (k < pivotNewIndex) {
debug ( "less " + k + " " + pivotNewIndex);
return select (list, k, left, pivotNewIndex - 1);
} else {
debug ( "great " + k + " " + pivotNewIndex);
return select (list, k, pivotNewIndex + 1 , right);
}
} //end func
这里用到了递归. 为了简化, 可以消除递归:
public static <E extends Comparable<? super E>> E select(List<E> list, int k, int left, int right) {
//select a pivot value list[pivotIndex]
while ( true ) {
int pivotIndex = (left + right) / 2;
int pivotNewIndex = partition (list, left, right, pivotIndex);
if (k == pivotNewIndex) {
return list.get(k);
}
if (k < pivotNewIndex) {
right = pivotNewIndex - 1;
} else {
debug ( "great " + k + " " + pivotNewIndex);
left = pivotNewIndex + 1;
}
}
} //end func
最后写个包装的函数, 该函数返回list中第k小的元素. 要求List中的元素实现Comparable接口.
public static <E extends Comparable<? super E>> E select(List<E> list , int k) {
if (list == null && list .size() == 0) {
return null ;
} //end if
return select (list, k, 0, list.size() - 1);
} //end func
性能测试:
如果元素数目少, 基本上体现不出性能的好处. 如果数目多的话, 就可以看出来.
我测试 1000000 个 Interger 的 List, 和使用Collections.sort相比,
Hoare算法: 172ms
Collections.sort: 1390ms
差距还是挺大的.
参考:
http://en.wikipedia.org/wiki/Selection_algorithm
http://hi.baidu.com/lff0305/blog/item/c1b6a07e5c141c3f0dd7da25.html