蓝桥杯第九届 javaB省赛 五、快速排序(详解每行代码)

题目描述:

以下代码可以从数组a[]中找出第k小的元素。  


它使用了类似快速排序中的分治算法,期望时间复杂度是O(N)的。


请仔细阅读分析源码,填写划线部分缺失的内容。

import java.util.Random;
public class Main{
	public static int quickSelect(int a[], int l, int r, int k) {
		Random rand = new Random();
		int p = rand.nextInt(r - l + 1) + l;
		int x = a[p];
		int tmp = a[p]; a[p] = a[r]; a[r] = tmp;
		int i = l, j = r;
		while(i < j) {
                	while(i < j && a[i] < x) i++;
                	if(i < j) {
                        	a[j] = a[i];
                        	j--;
                	}
                	while(i < j && a[j] > x) j--;
                	if(i < j) {
                        	a[i] = a[j];
                        	i++;
                	}
        	}
        	a[i] = x;
        	p = i;
        	if(i - l + 1 == k) return a[i];
        	if(i - l + 1 < k) return quickSelect( _________________________________ ); //填空
        	else return quickSelect(a, l, i - 1, k);	
	}
	public static void main(String args[]) {
		int [] a = {1, 4, 2, 8, 5, 7};
		System.out.println(quickSelect(a, 0, 5, 4));
	}
}

思路

代码的主要思想是使用快速排序,一定要会这个算法的代码才能够做这道题。关于快速排序的算法详解可以参考我的另一篇博客:蓝桥杯自学算法之快速排序

一、首先我们把代码的身体“腰斩”一下,先看前半部分代码:

(1)参数部分

一开始我很迷惑,为什么一个快速排序会出现随机数?然后仔细阅读代码后我才发现,这个随机数其实是为了选取快速排序的“基准数”,是分治的一种思想。后续确定基准数的位置后,在此时的数组中,基准数的左侧部分都小于等于这个基准数,右侧部分都大于等于这个基准数。

①参数p:

int p = rand.nextInt(r - l + 1) + l;

就是基准数的下标。我一开始想,奇怪了,为什么要那么麻烦,直接在数组的范围 [0, a.length-1] 内挑一个数作为基准数不就好了嘛,直接 int p = rand.nextInt(a.length - 1); 他不香嘛。但是要注意,这个随机数的产生的代码是在递归方法内的,比如当递归到右半部分的数组时,如果还用上面的代码选择基准数,那就出错了,因为按照上面的代码选出的基准数有可能不在右半部分数组内,比如p=0,明显它不在右半部分数组内。

因此,选择基准数时,先计算递归部分的长度 (r - l + 1),然后加上递归部分左边界 l,才是生成的基准数的范围。

②参数x:

int x = a[p];

用来备份基准数的值。

这样基准数在数组中所在的位置相当于空一个位置了,可以让其他位置的值填补这个坑而不会造成基准数的值丢失(实际上题目是把递归部分最右侧位置的值填到了这个坑里)。可以这么说,【此时a[p]所在的位置是空的】

③交换代码:

int tmp = a[p]; a[p] = a[r]; a[r] = tmp;

与其说“a[p]与递归部分最右侧的值进行交换”,不如说“把递归部分最右侧的值填到a[p]这个位置”。填补后,递归部分最右侧的位置为空(即 【此时a[r]所在的位置是空的】,因为他已经跑到a[p]的位置了)。

④参数i,j

	int i = l, j = r;

这个没什么可说的,i,j分别是左右边界指针,i会向右扫描,j会向左扫描

(2)循环部分:

	while(i < j) {	//很明显是快速排序的思路
		//指针i先从左侧开始扫描,如果i下标的值小于基准数,不管它,继续扫描
		while(i < j && a[i] < x) 
			i++; 
		if(i < j) {	//退出上面循环,说明碰到了比基准数大的数了
	    	a[j] = a[i];	//把这个大的数a[i]填到右侧指针j所在的位置, 【此时a[i]所在的位置是空的】
	        j--;    //因为j位置已经确定了数字了,j可以向左移一位
		}
		//随后指针j从右侧开始扫描,如果j下标的值大于基准数,不管它,继续扫描
	  	while(i < j && a[j] > x) 
	  		j--;
	 	if(i < j) {	//退出上面循环,说明碰到了比基准数小的数了
	      	a[i] = a[j];	//把这个小的数a[j]填到左侧指针i所在的位置,【此时a[j]所在的位置是空的】
	        i++;	//因为i位置已经确定了数字了,i可以向右移一位
	  	}
	  	//执行到了这里,就又倒回去,继续让指针i从左侧开始扫描。
	}

二、递归部分,也就是后半段代码

	a[i] = x;	//经过上面的循环下来,i和j相遇了,此时i=j,那么i或j就是基准数的位置了(重点!!!!!),所以把x填到a[i]中。
	p = i;		//不是很懂这一步,不管它
	if(i - l + 1 == k) 
		return a[i];
	if(i - l + 1 < k) 
		return quickSelect( _________________________________ ); //填空
	else 
		return quickSelect(a, l, i - 1, k);		//这一部分显然是左侧递归
	}

三、看不懂递归部分没关系,接下来我们举几个例子来走一遍代码。

********************例1:

现有数组:

【1,2,3,4,5,6,7】

我现在要找第4小的数(k=4)。开头选取随机数,数字4先生中奖了,他成为了基准数:

【1,2,3,[4],5,6,7】	k=4		l=0		r=6

好的,很完美,一路下来,完全不用交换任何一个数字,就可以直接进入递归判断了:

if(i - l + 1 == k) 
	return a[i];

变量i是基准数的下标,变量l是递归部分左侧边界,i = 3, l = 0

很巧妙地,3 - 0 + 1 == 4,条件符合,返回 a[i] 的值,也就是4;

*********************例2:

现有数组:

【1,2,3,4,5,6,7】

我现在要找第6小的数(k=6)。开头选取随机数,数字4先生中奖了,他成为了基准数:

【1,2,3,[4],5,6,7】	k=6		l=0		r=6

好的,也很完美,一路下来,完全不用交换任何一个数字,就可以直接进入递归判断了:

	if(i - l + 1 == k) 
		return a[i];
	if(i - l + 1 < k) 
		return quickSelect( _________________________________ ); //填空

计算 if 语句(为啥 if 语句要写成这样?通过下面过程来推断):(i - l + 1) = (3 - 0 + 1) = 4记住这个(i - l + 1)=4!!这个4代表的是【左侧部分(包括基准数)】的长度

很遗憾,他不能通过第一个判断。因此只能进入填空部分的递归。很显然这个是右半部分递归,参数是啥没关系,咱们强行进入下一层递归:

*****************例2的下一层递归

此时的数组右部分进行了递归(由于递归时传入了新的k,此时的k不知道是什么值):

【1,2,3,4,【5,6,7】		k=?		l=4		r=6

选取随机数,数字6先生刚好中奖了,他成为了基准数:

【1,2,3,4,【5,[6],7】	k=?		l=4		r=6

好的,这下也完美了,一路下来,完全不用交换任何一个数字,就可以直接进入递归判断了:

	if(i - l + 1 == k) 
		return a[i];

此时无论怎么说,他必须要进到第一个if判断里输出这个基准数6,不然不符合常理。 因为我们已经找到了一开始所说的第6小的数

而此时i = 5, l = 4, (i - l + 1) == 2,很容易发现,此时的k应该要等于2,才能进入判断输出值。我们再来观察一下已经递归到第二层的数组:

【1,2,3,4,【5,[6],7】		k=2		l=4		r=6

此时的k=2是什么意思?

右部分数组看成一个整体,这个k的意思就是这个整体的 “第k小” 的值!那么问题来了,第二层递归时传入的参数k=2是怎么变过来的呢?

还记得第一层提到的,(i - l + 1)= 4 的意思是左侧部分数组(包括基准数)的长度。这里的k=2 显然是由 上一层的k 减去 (i - l + 1) 得到的,即:k - (i-l+1) = 6 - 4 = 2,直观一点来说:

第一层递归(4是基准数,我要找第6小的数):
【1,2,3,[4],5,6,7】		k=6		l=0		r=6

把k减去以下长度(因为我即将要递归右部分数组):
【1,2,3,[4]】 这个长度为:(i - l + 1)

进入第二层递归(6是基准数,我现在要找第2小的数):
【5,[6],7】		k=2		l=4		r=6

所以,填空部分应该是:

	if(i - l + 1 == k) 
		return a[i];
	if(i - l + 1 < k) 
		return quickSelect( a, i + 1, k - (i-l+1) ); //填空
	else 
		return quickSelect(a, l, i - 1, k);		//这一部分显然是左侧递归

总结

这道题突破的关键是明白最后一个参数k的含义。

把数组的该层递归部分 [l, r] 当成一个整体,k的意思就是该整体的 “第k小” 的值。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值