1. 程式人生 > >解決尋找第K小元素問題——三種不同的演算法實現

解決尋找第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)。