解決尋找第K小元素問題——三種不同的演算法實現
問題描述:在一個序列裡找出第K小元素
以下程式基於函式 int select_kth_smallest(list q, int k) 實現 :返回向量q中第k最小元的函式
演算法一:
基於氣泡排序思想,暴力求解:
基本思路:要求找出第k個最小元素,可以通過在序列中遍歷k次,每次找出最小的,並放在序列頭。類似泡泡一樣,找出第k個大的泡泡(bubble)
虛擬碼:
int select_kth_smallest(list q, int k){ intput: q is a list, k is the number of which the kth smallest in list output: return the value of the k largest number in list size <- q.size for i <- 0:k-1 do min <- q[i] index <- i for j <- i+1 : size-1 do if min > q[j] do min <- q[j] index <- j endif endfor q[index] <- q[i] q[i] <- min endfor return q[k-1] }
程式碼:
int select_kth_smallest(vector<int> q, int k){
int size = q.size();
for(int a=0; a<k; a++){
int min = q[a], index = a;
for(int b=a+1; b<size; b++)
if(min > q[b]){
min = q[b];
index = b;
}
q[index] = q[a];
q[a] = min;
}
return q[k-1];
}
演算法複雜度分析:
由於都是在原地實現,所以空間複雜度為O(n)
尋找第k小元素,總共遍歷了k次序列。T = n + (n-1) + (n-2) + ...... + (n-k-1) = kn - (1+2+...+k-1) = O(nk)
演算法二:
基於快速排序,高效求解
基本思路:每次以序列第一個元素作為中間數mid,將序列中小於等於mid的放在mid左邊,反之放在mid右邊。不妨設小於等於mid的個數為i,若k < i,則對左邊序列遞迴做相同操作;若k > i, 則對右邊序列遞迴尋找序列中第k-i小的元素;若k = i,則mid即為第k小的元素
虛擬碼:
int select_kth_smallest(list q, int k){ intput: q is a list, k is the number of which the kth smallest in list output: return the value of the k largest number in list bot <- 0 top <- size of q while bot < top do index <- q[bot] left <- bot right <- bot while right < top do if q[right] < index do left <- left + 1 temp <- q[right] q[right] <- q[left] q[left] <- temp right <- right + 1 endwhile q[left] <- q[bot] q[bot] <- index if left + 1 < k do bot <- left + 1 else if left + 1 > k do top = left else do return q[left] endif return -1 }
程式碼:
int select_kth_smallest(vector<int> q, int k){
int bot = 0, top = q.size();
while(bot < top){
int left = bot, right = bot, index = q[bot];
while(right < top){
if(q[right] < index){
left++;
int temp = q[left];
q[left] = q[right];
q[right] = temp;
}
right++;
}
/*
for(int a=0; a<q.size(); a++)
cout<< q[a]<< ' ';
cout<< endl;
cout<< bot<< ' '<< top<< endl;
*/
q[bot] = q[left];
q[left] = index;
if(left+1 < k)
bot = left + 1;
else if(left+1 > k)
top = left;
else
return q[left];
}
return -1;
}
演算法複雜度分析:
空間複雜度:原地操作,O(n)
時間複雜度:由於將序列第一個元素作為mid來分開序列,具有一定的隨機性。所以我們只能考慮極限情況,最壞情況時,即每次選的都是最大值或者最小值,則需要進行k次迴圈,T = O(n^2)。
演算法三:BFPRT(線性查詢演算法)
BFPRT演算法描述:
從某n個元素的序列中選出第k大(第k小)的元素,通過巧妙的分 析,BFPRT可以保證在最壞情況下仍為線性時間複雜度。該演算法的思想與快速排序思想相似,當然,為使得演算法在最壞情況下,依然能達到o(n)的時間複雜 度,五位演算法作者做了精妙的處理。
演算法步驟:
1. 將n個元素每5個一組,分成n/5(上界)組。
2. 取出每一組的中位數,任意排序方法,比如插入排序。
3. 遞迴的呼叫selection演算法查詢上一步中所有中位數的中位數,設為x,偶數箇中位數的情況下設定為選取中間小的一個。
4. 用x來分割陣列,設小於等於x的個數為k,大於x的個數即為n-k。
5. 若i==k,返回x;若i<k,在小於x的元素中遞迴查詢第i小的元素;若i>k,在大於x的元素中遞迴查詢第i-k小的元素。
終止條件:n=1時,返回的即是i小元素。
虛擬碼:
int select_kth_smallest(list q, int k){
intput: q is a list, k is the number of which the kth smallest in list
output: return the value of the k largest number in list
bot <- 0
top <- the size of q
while bot < top do
left <- bot
right <- bot
index <- choose_mid(bot, top)
while right < top do
if q[right] < index do
left <- left + 1
temp <- q[right]
q[right] <- q[left]
q[left] <- temp
endif
right <- right + 1
endwhile
if left + 1 < k do
bot <- left + 1
else if left + 1 > k do
top = left
else do
return q[left]
endif
endwhile
return -1
}
int choose_mid(q, left, right){
input: q is a number list, left and right are the indexes of q indicating the range of choosing midian
output: the median in range of (left, right)
v <- [] // create a empty vector
while left + 5 < right do
mid <- selection(q[left : left+5])
push mid in v
left <- left + 5
endwhile
mid <- selection(q[left : right])
push mid in v
return selection(v)
}
int selection(v){
input: v is a number vector
output: return the midian in v
size <- the size of v
for a <- 1:size-1
b <- a
temp <- v[b]
while b > 0 and temp < v[b-1] do
v[b] <- v[b-1]
b <- b - 1
v[b] <- temp
index <- (size-1) // 2
return v[index]
}
程式碼:
int select_kth_smallest(vector<int> q, size_t k);
int choose_mid(vector<int>& q, int left, int right);
int selection(vector<int> v);
int select_kth_smallest(vector<int> q, size_t k){
int bot = 0, top = q.size();
while(bot < top){
int left = bot, right = bot, i;
int index = choose_mid(q, bot, top);
while(right < top){
if(q[right] < index){
int temp = q[left];
q[left] = q[right];
q[right] = temp;
left++;
}
if(q[right] == index)
i = right;
right++;
}
q[i] = q[left];
q[left] = index;
/*
for(int a=0; a<q.size(); a++)
cout<< q[a]<< ' ';
cout<< endl;
cout<< index<< endl;
cout<< bot<< ' '<< top<< endl;
cout<< left<< ' '<< right<< endl;
*/
if(left+1 < k)
bot = left + 1;
else if(left+1 > k)
top = left;
else
return index;
}
return -1;
}
int choose_mid(vector<int>& q, int left, int right){
vector<int> v;
while(left+5 < right){
int mid = selection(vector<int>(&q[left], &q[left+5]));
v.push_back(mid);
left += 5;
}
int mid = selection(vector<int>(&q[left], &q[right]));
v.push_back(mid);
return selection(v);
}
int selection(vector<int> v){
int size = v.size();
for(int a=1; a<size; a++){
int b = a;
int temp = v[b];
while(b>0 && temp<v[b-1]){
v[b] = v[b-1];
b--;
}
v[b] = temp;
}
int mid = (size-1)/2;
return v[mid];
}
演算法分析:
在第三步選出x時,大於x的元素至少有3n/10 - 6個,在最差的情況下,第五步遞迴作用的元素個數是7n/10 + 6.可得遞迴表示式如下:
T(n) <= T(n/5) + T(7n/10 + 6) + O(n)
比起演算法二,消除了中間數mid選取的隨機性干擾,使得在最差的情況下也是線性複雜度O(n)。