對於給定的n個元素的數組a[0 : n - 1],要求從中找出第k小的元素。當a[0 : n - 1]被排序時,該元素就是a[k - 1]。假設n=8,每個元素有兩個域k e y和I D,其中k e y是一個整數,I D是一個字元。假設這8個元素為[( 1 2 ,a),( 4 ,b),( 5 ,c),( 4 ,d),( 5 ,e),( 1 0 ,f),( 2 ,g),( 2 0 ,h)], 排序後得到數組[( 2 ,g),( 4 ,d),( 4 ,b),( 5 ,c),( 5 ,e),( 1 0 ,f),( 1 2 ,a),( 2 0 ,h)]。如果k=1,返回I D為g 的元素;如果k=8,返回I D為h 的元素;如果k=6,返回是I D為f 的元素;如果k=2,返回I D為d 的元素。實際上,對最後一種情況,所得到的結果可能不唯一,因為排序過程中既可能將I D為d 的元素排在a[1],也可能將I D為b 的元素排在a[1],原因是它們具有相同大小的k e y,因而兩個元素中的任何一個都有可能被返回。但是無論如何,如果一個元素在k=2時被返回,另一個就必須在k=3時被返回。
選擇問題的一個應用就是尋找中值元素,此時k=[n / 2]。中值是一個很有用的統計量,例如中間工資,中間年齡,中間重量。其他k值也是有用的。例如,通過尋找第n / 4 , n / 2和3 n / 4這三個元素,可將人口劃分為4份。
選擇問題可在O( n l o g n )時間內解決,方法是首先對這n個元素進行排序(如使用堆排序式或歸併排序),然後取出a[k - 1]中的元素。若使用快速排序(如圖1 4 - 11所示),可以獲得更好的平均效能,儘管該演算法有一個比較差的漸近複雜性O( n2 )。
可以通過修寫程式1 4 - 6來解決選擇問題。如果在執行兩個w h i l e迴圈後支點元素a[l]被交換到a[j] ,那麼a[l]是a[l : j]中的第j - l + 1個元素。如果要尋找的第k個元素在a[l : r]中,並且j - l + 1等於k,則答案就是a[l];如果j - l + 1 <k,那麼尋找的元素是r i g h t中的第k - j + l - 1個元素,否則要尋找的元素是left中的第k個元素。因此,只需進行0次或1次遞迴調用。新代碼見程式1 4 - 7。S e l e c t中的遞迴調用可用f o r或w h i l e迴圈來替代(練習2 5)。
程式14-7 尋找第k個元素
template
T Select(T a[], int n, int k)
{// 返回a[0 : n - 1]中第k小的元素
// 假定a[n]是一個偽最大元素
if(k <1 || k >n) throw OutOfBounds();
return select(a, 0, n-1, k);
}
template
T select(T a[], int l, int r, int k)
{// 在a[l : r]中選擇第k小的元素
if(l >=r) return a[l];
int i=l, // 從左至右的遊標
j=r + 1; // 從右至左的遊標
T pivot=a[l];
// 把左側>=pivot的元素與右側<=pivot 的元素進行交換
while(true) {
do {// 在左側尋找>=pivot 的元素
i=i + 1;
} while(a[i] <pivot);
do {// 在右側尋找<=pivot 的元素
j=j - 1;
} while(a[j] >pivot);
if(i >=j) break; // 未發現交換對象
Swap(a[i], a[j]);
}
if(j - l + 1==k) return pivot;
// 設定p i v o t
a[l]=a[j];
a[j]=pivot;
// 對一個段進行遞迴調用
if(j - l + 1 <k)
return select(a, j+1, r, k-j+l-1);
else return select(a, l, j-1, k);
}