Select 算法
I 編程珠璣(續)介紹的 Quickselect 算法
選擇 N 個元素中的第 K 小(大)值,是日常場景中常見的問題,也是經典的算法問題.
選取 N 個元素的數組的中的第 K 小(大)值,最簡單的想法是將數組排序后直接選取. 那么這種方法的時間復雜度是O(N log N).
C.A.R.Hoare 提出的 Quickelect 算法的平均時間復雜度達到了 O(N) . 在去遞歸之后, 是原地算法. 這個算法因為其簡潔,高效而被廣泛使用.
算法思路的C++實現如下.
int select(vector<int>& X, int k) {
int l = 0, u = X.size() - 1;
while(l < u){
swap(X[l], X[rand()%(u-l+1)+l]);
int m = l;
for(int i = l + 1; i <= u; i++)
if(X[i] < X[l])
swap(X[++m], X[i]); //m在i遍歷的過程中,是遍歷過的元素中, 小於X[l]的元素的最大下標
swap(X[l], X[m]);
if(k <= m) u = m - 1;
if(k >= m) l = m + 1;
}
return X[k];
}
- 當 k 選定為數組的中位數時,平均所耗的時間最多.
- 當數組中有大量重復元素,或者是逆序排序的數組時,會增加運行時間. 遇到大量重復的元素時不能很快地縮小 l - u 的范圍. 逆序數組會產生很多的 swap 操作.
- Worst-case peformance O(N ^ 2)
II 序列輸入時使用的 Heap-Select 算法
考慮一個輸入序列,要求在序列輸入完畢的時候得出這個序列的第 k 大(小)的元素.
要選擇第 k 小的元素時, 我們考慮用一個 k 大小的大頂堆. 對數組從頭開始遍歷(等價於數組線性輸入), 頭 k 個元素用於建立 k 大小的大頂堆. 對於從 k + 1 到 N 的元素. 當該元素小於堆頂元素的時候,將該元素插入到堆中,將堆頂元素出堆. 遍歷(輸入)結束后, 堆頂元素即為我們要找的元素.
相應的選擇第 k 大的元素時, 我們考慮用一個 k 大小的小頂堆.對數組從頭開始遍歷. 頭 k 個元素用於建立 k 大小的小頂堆. 對於從 k + 1 到 N 的元素. 當該元素大於堆頂元素的時候,將該元素插入到堆中,將堆頂元素出堆. 遍歷(輸入)結束后, 堆頂元素即為我們要找的元素.
這樣可得這個算法的時間復雜度為 O(k) + O(N * log k) ==> O(N * log k)
由於要調用空間構造堆,空間復雜度為 O(k)
關於這個算法的正確性,用歸納法, 從已經輸入k的數組中挑選頭k個最大(小)的元素。 然后繼續下去即可。
III 三個元素的中間值
殺雞不用牛刀,三個元素的中間值用簡單的三次比較就可以搞定.
if(X[1] > X[2])
swap(X[1], X[2]);
if(X[2] > X[3])
swap(X[2], X[3]);
if(X[1] > X[2])
swap(X[1], X[2]); //自此 X[1], X[2], X[3] 從小到大有序.
IV 其他的Select算法
Median of medians 又名 BFPRT算法. 基於Blum, Floyd, Pratt, Rivest and Tarjan 1973年的論文 Time Bounds for Selection. 擁有O(N) 的 worst case performance.
Introselect 則是BFPRT算法和 Quickselect 算法的結合. 默認使用 Quickselect ,在 Quickselect 表現出比較差的運行情況時轉向Median of medians. 從而也能提供O(N) 的 worst case performance.