選擇問題(求第k個最小元素)
什麼是選擇問題
選擇問題(selection problem)是求一個n
個數列表的第k
個最小元素的問題。這個數字被稱為第k
個順序統計量(order statistic)。當然,對於k=1
或者k=n
的情況,我們可以掃描整個列表,找出最小或者最大的元素。對於其他情況,我們可以對列表進行排序,然後返回第k
個元素。
可是,對於整個列表進行排序是不是小題大做?因為該問題僅僅是要找出第k
小的元素,而不是要求把列表從小到大排列。
劃分的思路
我們可以將給定的列表根據某個值p
(例如列表的第一個元素)進行劃分。一般來說,這是對列表元素的重新整理,使左邊部分包含所有小於等於p的元素,緊接著是中軸(pivot)p
本身,再接著是所有大於等於p的元素。如下圖所示
有兩種主要的劃分方法,本文討論 Lomuto 劃分,以後會介紹更有名的 Hoare 劃分。
Lomuto 劃分
Lomuto(洛穆託)劃分的虛擬碼如下:
// 演算法:Lomuto_Partition(A[l..r])
// 用第一個元素作為中軸對子陣列進行劃分
// 輸入:陣列A[0..n-1]的一個子陣列A[l..r],它由左右兩邊的索引l和r(l<=r)定義
// 輸出:A[l..r]的劃分和中軸的新位置
p = A[l]
s = l
for i=l+1 to r do
if A[i] < p
s = s+1;
swap(A[s], A[i])
swap(A[l], A[s])
return s
利用劃分求第k
小元素
我們如何利用劃分列表來尋找其第k
小元素呢?
假設列表是以陣列實現的,其元素索引從0
開始,那麼第k
小的元素就是把此列表從小到大排序後,索引在k-1
位置上的元素。
假設首次劃分此列表,s是分割位置,也就是劃分後中軸元素的索引。我們分3種情況進行討論:
[1]. 當s=k-1
,那麼中軸p
本身顯然就是第k
小的元素;
[2]. 如果s>k-1
,那麼整個列表的第k
小元素就是左邊部分的第k
小元素;
[3]. 如果s<k-1
,那麼問題就轉換為求右邊部分的第(k-s-1)
小元素;推導過程是這樣的:本來是求第k
小,通過劃分,篩除了最前面的(s+1)
個元素,所以只用求右邊部分(藍色)的第 k-(s+1)
小。
可以看出,第2種情況和第3種情況雖然沒有徹底解決問題,但是使問題的例項變小了。對於這個較小的例項可以用同樣的方法來解決,即遞迴求解。這個演算法被稱為“快速選擇”,在演算法思想中屬於減可變規模演算法(減治法的一種)。
C語言實現
// 交換*a和*b
void swap(int* a, int* b)
{
int temp = *a;
*a = *b;
*b = temp;
}
// a是陣列首地址,l和r分別是兩端的索引,要求l<=r
int Lomuto_partition(int a[], int l, int r)
{
int pivot = a[l];
int j = l;
for(int i = l + 1; i <= r; ++i)
{
if( a[i] < pivot )
{
swap( &a[++j], &a[i]);
}
}
swap(&a[l], &a[j]);
return j; // j是下標
}
// 返回第k小的元素值。
// 注意:k不是下標,表示第k個
int __quick_select(int a[], int l, int r, int k)
{
// s是分裂點,中軸的下標
int s = Lomuto_partition(a, l, r);
if(s == l+k-1)
return a[s];
else if( s > (l+k-1) )// 處理左邊的部分
return __quick_select(a, l, s-1, k);
else // 處理右邊的部分
return __quick_select(a, s+1, r, k-(s-l+1));
}
// 快速選擇主函式
int quick_select_min(int a[], int len, int k)
{
return __quick_select(a, 0, len-1, k);
}
測試程式碼如下:
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int k = atoi(argv[1]); //把命令列輸入的字串轉為整數
printf("k = %d \n",k);
int array[] = {9,9,8,7,6,5,4,3,3,3,2,2,2,1,1,0,};
int len = sizeof array/sizeof array[0];
printf("%dth min = %d\n", k, quick_select_min(array,len,k) );
return 0;
}
執行截圖:
改進
該演算法也可以不用遞迴實現。在非遞迴版本中,甚至不需要調整k
的值,只要一直呼叫Lomuto_partition
,直到s=k-1
為止。
程式碼如下:
int quick_select_min_2(int a[], int len, int k)
{
int left = 0;
int right = len - 1;
int s;
// 直到返回的下標是 k-1 為止
while( (s = Lomuto_partition(a, left, right)) != (k-1) )
{
if(s < k-1)
left = s+1; //處理右邊的部分
else
right = s-1; // 處理左邊的部分
}
return a[s];
}
參考資料
《演算法設計與分析基礎(第3版)》(清華大學出版社)
相關文章
- 選擇問題——選取第K小元素
- 用PriorityQueue解決選擇最小的K個數問題
- TopK問題,陣列中第K大(小)個元素問題總結TopK陣列
- 最小的 k 個元素--快排變形
- 【谷歌面試題】求陣列中兩個元素的最小距離谷歌面試題陣列
- 第k大元素
- 使用simplemind如何選擇多個元素
- 陣列中的第K個最大元素陣列
- [每日一題] 第二十題:最小的k個數每日一題
- DOM元素的選擇
- CSS 元素選擇器CSS
- jq選擇子元素
- jQuery選擇器獲取前幾個元素jQuery
- jQuery點選的是第幾個li元素jQuery
- jQuery選擇器——子元素過濾選擇器jQuery
- 獲取一個陣列裡面第K大的元素陣列
- 215. 陣列中的第K個最大元素陣列
- 如何選擇元素定位方式
- jQuery選擇器——表單元素過濾選擇器jQuery
- 這就是選擇排序的問題排序
- CSS選擇器常見問題CSS
- :eq()選擇器匹配多個元素程式碼例項
- CSS5-選擇器5-子元素選擇器CSS
- 【爬坑日記】.class.class選擇器的選擇問題
- CSS選擇器(6)——偽元素CSS
- 選擇 NoSQL 資料庫需要考慮的 10 個問題SQL資料庫
- 流量卡哪個最划算?解決流量卡選擇問題
- ListView Item 選擇問題解決之道View
- 力扣-215. 陣列中的第K個最大元素力扣陣列
- CSS3新增選擇器(屬性選擇器、結構偽類選擇器、偽元素選擇器)CSSS3
- 尋找陣列中第K大的元素陣列
- 查詢陣列中第K大的元素陣列
- 【遞迴打卡2】求兩個有序陣列的第K小數遞迴陣列
- 政府OA系統選擇時要考慮的六個問題
- CSS E::after 偽元素選擇器CSS
- jQuery子元素過濾選擇器jQuery
- jQuery選擇器 標籤選擇元素+css簡單新增移除操作jQueryCSS
- LeetCode-215-陣列中的第K個最大元素LeetCode陣列