这两天因为一直有面试,所以没有更新博客,不过每天还是会保持做几道算法题,但时间问题没能和大家交流,今天我会多分享一些知识希望对大家能有所帮助。那么在这篇博文中我就来和大家深入探讨一下怎样从数组中找出最小的k个数吧,这是阿里一面的时候问我的一道算法题,后来又翻了一下书发现是剑指offer上的原题,但当时候因为电面答的也不是很好,有点懊悔呃呃~刚听到这道题的时候,想了想直接排序然后查找就好了不是么,然后无脑的说了被问时间复杂度是多少,我回答了O(nlogn),面试官问时间复杂度还能再小么,后来想想要这么简单面试官又怎么会问呀,苦逼了几分钟后胡乱讲了莫名其妙的一些解法都被否认了,然后转向了下一个提问......因此结束了面试,赶快去查阅了资料才明白了其中的奥秘,唉,进步的空间还是忒大了~
首先,我们可以基于快速排序的算法来调整,当然这种排序会修改我们输入的数组,寻找最小的k个数,那么如果k左边的数都小于k位置的数,k右边的数都大于k位置的数,则找到k的位置即可,具体实现如下:
public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
ArrayList<Integer> res = new ArrayList<Integer>();
if (input==null||input.length==0||input.length<k||k<=0) {
return res;
}
int start = 0;
int end = input.length-1;
int index = partition(input, start, end);
while (index != k - 1) {
if (index > k - 1) {
end = index-1;
index = partition(input, start, end);
} else {
start = index+1;
index = partition(input, start, end);
}
}
for (int i = 0; i < k; i++) {
res.add(input[i]);
}
return res;
}
static int partition(int input[], int start, int end) {
int tmp = input[start];
while (start < end) {
while (start < end && input[end] >= tmp) {
end--;
}
input[start] = input[end];
while (start < end && tmp >= input[start]) {
start++;
}
input[end] = input[start];
}
input[start] = tmp;
return start;
}
也许很多人会问,这种算法的时间复杂度是多少呢,答案是O(n),为什么~
我们都知道快排是向下递归的,那么在平均或者说是期望情况下它找到的永远是中间位置,这就有点类似折半查找了,所以第一次它会对n个数进行划分,第二次循环它只会进入一边,然后对n/2个数进行划分,以此类推,总共需要对n+n/2+n/4+...+2+1=n(1+1/2+1/4+...)+2+1个数进行操作,1+1/2+1/4+...<2,因此该表达式小于2n,所以它的期望时间复杂度为O(n)。
前面我也说了,这种方法会改变原数组中元素的位置,如果面试官不允许改变数组元素的位置呢,那么我们就只能使用另外一种算法了,这种算法可以处理大量数据,此时大家应该会想到这种算法和堆有关了吧,我们可以先创建一个大小为k的数据容器来存储最小的k个数字,接下来每次从输入的n个整数中读入一个数,如果容器中已有的数字少于k个,则直接把这次读入的整数放入容器之中,如果容器中已有k个数字了,也就是容器已满,此时我们不能再插入新的数字而只能替换已有的数字,找出这已有的k个数中的最大值,然后拿这次待插入的整数和最大值进行比较,小则替换当前最大值,大则不可能是最小的k个整数之一,于是我们可以抛弃这个整数。
因此,当容器满了之后,我们需要进行3步操作:一是在k个整数中找到最大数;二是有可能从这个容器中删除最大值;三是可能插入一个新的数字。如果用一颗二叉树来实现这个数据容器,那么我们能在O(logk)时间内实现这三步操作,因此,对于n个输入数字而言,总的时间效率就是O(nlogk)。
我们可以选择用不同的二叉树来实现这个数据容器。由于每次都需要找到k个整数中的最大数字,我们很容易想到最大堆,在最大堆中,根节点的值总是大于它的子树中任意结点的值。于是我们每次可以在O(1)时间内的到已有的k个数字的最大值,但需要O(logk)时间完成删除即插入操作。
下面是代码实现:
static public ArrayList<Integer> GetLeastNumbers_Solution1(int[] input, int k) {
ArrayList<Integer> res = new ArrayList<Integer>();
if (input==null||input.length==0||input.length<k) {
return res;
}
int []maxHeap = new int[k];
for (int i = 0; i < maxHeap.length; i++) {
maxHeap[i] = input[i];
}
for (int i = (maxHeap.length-1)/2; i >=0 ; i--) {
adjustHeap(maxHeap, i);
}
for (int i = k; i <input.length ; i++) {
if (maxHeap[0]>input[i]) {
maxHeap[0] = input[i];
adjustHeap(maxHeap, 0);
}
}
for (int i = 0; i < maxHeap.length; i++) {
res.add(maxHeap[i]);
}
return res;
}
static void adjustHeap(int maxHeap[],int i){
int index = i;
int lchild=2*i+1; //i的左孩子节点序号
int rchild=2*i+2; //i的右孩子节点序号
if(index<=(maxHeap.length-1)/2) {
//寻找子节点中最大的节点
if (lchild<maxHeap.length&&maxHeap[index]<maxHeap[lchild]) {
index = lchild;
}
if (rchild<maxHeap.length&&maxHeap[index]<maxHeap[rchild]) {
index = rchild;
}
if (i!=index) {
//将节点与最大的子节点交换
int tmp = maxHeap[index];
maxHeap[index] = maxHeap[i];
maxHeap[i] = tmp;
//交换后,子树可能不满足最大推,递归调整。
adjustHeap(maxHeap, index);
}
}
那么在这里我同样解释一下时间复杂度吧,从主函数中循环可以看出循环次数为n-k,当n特别大且k特别小时,n-k近似为n,然后在每次循环中进行上述时间复杂度为O(logk)的三个步骤,所以总的时间复杂度为O(nlogk)。
至此相信大家已经理解这个问题了哈,在面试时,如果遇到的问题有多种解法,每种解法都有自己的优缺点,那么我们首先应该向面试官问清楚题目的要求,输入的特点,从而选择最合适的解法。