要在一個序列里找出第K小元素,可以用排序算法,然后再找。可以證明,排序算法的上界為O(nlogn)。
在這里,給出兩種可以在線性時間內找出第K小元素的方法。
方法1:
(1) 選定一個比較小的閾值(如44),當序列元素小於閾值時,直接用排序算法來做;
(2) 當序列元素大於閾值時,把元素划分為以5個元素為一組,每一組元素自身作排序,然后挑出每一組元素的中間值,再在所有的中間值中,遞歸調用本算法,挑出中間值,可以認為,此值大約為整個序列的中間值(當序列元素個數不是5的倍數時,最后一組不足5的舍掉,這個對中間值影響不大);
(圖片參考《ALGORITHMS DESIGN TECHNIQUES AND ANALYSIS》 M. H. Alsuwaiyel)
(3) 把元素按中間值划分為三組,第一組小於中間值,第二組等於中間值,第三組大於中間值;
(4) 若第一組的元素個數大於等於K,即第K個元素在第一組內;若第一組和第二組的元素個數大於等於K,即中間值為第K個元素;否則,第K個元素在第三組,再遞歸調用本算法,注意K要減去一二組的元素個數。

public static int select(int[] A, int k){ return selectDo(A, 0, A.length-1, k); } private static int selectDo(int[] A, int low, int high, int k){ //select k min number int p = high - low + 1; if(p < 44){ Arrays.sort(A, low, high+1); return A[low+k]; } //A divided into q groups, each group 5 elements, and sort them int q = p/5; int[] M = new int[q]; for(int i = 0; i < q; i ++){ Arrays.sort(A, low + 5*i, low + 5*i + 5); M[i] = A[low+5*i+2]; } //select mid in M int mid = selectDo(A, 0, q-1, (q-1)/2); //A divided into 3 groups int[] A1 = new int[p]; int[] A2 = new int[p]; int[] A3 = new int[p]; int count1, count2, count3; count1 = count2 = count3 = 0; for(int i = low; i <= high; i ++){ if(A[i] < mid) A1[count1++] = A[i]; else if(A[i] == mid) A2[count2++] = A[i]; else A3[count3++] = A[i]; } if(count1 >= k) return selectDo(A1, 0, count1-1, k); if(count1 + count2 >= k) return mid; return selectDo(A3, 0, count3-1, k-count1-count2); }
這個方法雖然可以以最壞時間復雜度O(n),但是系數值會比較大,而且算法比較復雜。
方法2:
(1) 隨機挑選一個元素X作為數組的划分,此時數組分為三部分,第一部分是小於X的元素,第二部分只有一個元素就是X,第三部分是大於或等於X的元素;
(2) 當第一部分的元素個數N大於K時,說明第K個元素在第一部分;當第一部分的元素個數N等於K時,說明X就是第K個元素(注意到下標從0開始);否則,第K個元素在第三部分,再遞歸調用本算法,注意K要減去第一部分的元素個數再減1(包括X)。

public static int randomSelect(int[] A, int k){ return randomSelectDo(A, 0, A.length-1, k); } private static int randomSelectDo(int[] A, int low, int high, int k){ int i = randomPartition(A, low, high); //n is the number of < A[i] int n = i-low; if(n > k) return randomSelectDo(A, low, i-1, k); else if(n == k) return A[i]; else return randomSelectDo(A, i+1, high, k-n-1); } private static void swap(int[] A, int i, int j){ int temp = A[i]; A[i] = A[j]; A[j] = temp; } private static int randomPartition(int[] A, int low, int high){ //random divide Random rand = new Random(); int r = rand.nextInt(high-low+1) + low; swap(A, low, r); int i = low; int x = A[low]; for(int j = low+1; j <= high; j ++){ if(A[j] < x){ i ++; if(i != j){ swap(A, i, j); } } } swap(A, low, i); return i; }
此方法和快速排序有點相似,也是需要划分數組的方法;與方法1相比,不同之處在於划分元素的選擇。采用隨機化划分,在實際上可以達到O(n)的要求。而且算法簡潔優美。