解决寻找第K小元素问题——三种不同的算法实现

个人原创,禁止转载——Zetrue_Li 

问题描述:在一个序列里找出第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)。

猜你喜欢

转载自blog.csdn.net/weixin_37922777/article/details/82666466