从一个序列里面选择第k大的数在没有学习算法导论之前我想最通用的想法是给这个数组排序,然后按照排序结果返回第k大的数值。如果使用排序方法来做的话时间复杂度肯定至少为O(nlgn)。
问题是从序列中选择第k大的数完全没有必要来排序,可以采用分治法的思想解决这个问题。Randomize select 算法的期望时间复杂度可以达到O(n),这正是这个算法的迷人之处。具体的算法分析可以在《算法导论》这本书里查看。
贴出伪代码:
RANDOMIZED-SELECT(A, p, r, i) 1 if p = r 2 then return A[p] 3 q ← RANDOMIZED-PARTITION(A, p, r) 4 k ← q - p + 1 5 if i = k ▹ the pivot value is the answer 6 then return A[q] 7 elseif i < k 8 then return RANDOMIZED-SELECT(A, p, q - 1, i) 9 else return RANDOMIZED-SELECT(A, q + 1, r, i - k)
这个算法的思想其实跟quik-sort有些相似,采用分治法的思想来解决。首先选择一个主元pirvot: q,将序列中的元素分为两个集合Q,W,Q里面的元素都小于主元pirvot,W里面的元素都大于pirvot。然后递归的调用这个过程可以得到我们想要的第i大的元素。这里的划分有三种情况:
1:主元的选择正好是第i大的元素,那么返回这个元素即可
2:Q里面的元素个数 k=(q-p+1) 大于i,代表第i大的元素还在Q这个集合里,那么继续这个过程寻找第i小的元素( step 7-8)
3:Q里面的元素个数 k=(q-p+1) 小于i,代表已经找到了k个小的元素,那么第i小的元素一定在W这个集合里,只要在W集合里寻找第(i-k)小的元素即可
下面给出这个算法的java实现:
/** * 根据算法导论的伪代码,完成快速选择的代码。 * @author 截取自:http://blog.csdn.net/zy825316/article/details/19486167 */ public class randomizedSelect { /** * @param args */ public static void main(String[] args) { int a[]={2,5,3,0,2,3,0,3}; int result=randomizedSelect(a,0,a.length-1,3);//产生第三小的数 System.out.print(" "+result); } private static int partition(int[] a, int p, int r) { int x=a[r]; int i=p-1; for(int j=p;j<r;j++){ if(a[j]<=x){ i=i+1; swap(a, i, j); } } swap(a, i+1, r); return i+1; } private static int randomizedPartition(int[] a,int p,int r){ java.util.Random random = new java.util.Random(); int i=Math.abs(random.nextInt() % (r-p+1)+p);//产生指定范围内的随机数 swap(a,i,r); return partition(a,p,r); } /** * * @param a 数组 * @param p 数组的第一个元素 * @param r 数组的最后一个元素 * @param i 需要求第几小的元素 * @return */ private static int randomizedSelect(int[] a,int p,int r,int i){ if(p==r){ return a[p];//这种情况就是数组内只有一个元素 } int q=randomizedPartition(a,p,r); int k=q-p+1;//拿到上一句中作为枢纽的数是第几小的数 if(i==k){ return a[q]; }else if(i<k){ return randomizedSelect(a,p,q-1,i); }else{ return randomizedSelect(a,q+1,r,i-k); } } private static void swap(int[] a, int i, int j) { int temp=a[i]; a[i]=a[j]; a[j]=temp; } }