题目描述:
以下代码可以从数组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小” 的值。