题目链接:查找最小的K个数
本题,一般人的第一思路是用排序,然后取前k个,那么这个时间复杂度,就取决于排序算法的时间复杂度。最小的O(nlogn)。
虽然这个很好想,但在面试时只能做最初的方案(面试时很推荐最开始用这个算法,后面再改进,可以让面试官看到你思考的过程)。如果就是排序,没有后续的优化,那这道题答的就太失败了
接下来咱们就介绍两种优化的方式,具体用哪一个,看面试官怎么说
第一种:快速选择算法:时间复杂度O(N)
该算法脱胎自快速排序。与快排的区别在于,快排是对标志两边进行分治,而快选是对单侧进行分治。
算法思路:在数组中随机或者指定一个标志,将所有比这个标志的值小的,放到左边;比这个标志值大的放到右边。(这里的例子举的是最小的k个,最大的类似)
每一次这样排序进行完,标志的位置有三种情况:
1、这个位置就是k,那么直接返回就好。
2、这个位置小于K,那么说明,虽然左边的都比标志小但是数量不够,或者还没定到K的位置。说明K在右边,这里我们对位置右边进行递归。
3、这个位置大于K,那么说明,K就在左边,那么就对标志左边进行递归。
以上就是思路,但是还有一个点我们要去解决,就是怎么对数组进行排序。
这里我们就给出两种方法,具体用哪个,看读者理解哪种了
第一种:双指针(这里的指针,只是代表秩)
如上图,在区间[lo,hi)中,一个指针指lo,另一个指hi,标记的数在lo位置
hi先启动,向左搜索,如果发现某个值小于标记的数,就停下,开启第二阶段
lo启动,向有搜索,如果发现某个值大于标记的数,就与hi交换数据。以此循环直至hi指针 <= lo指针或者hi与lo到达另一边界。
这时,hi指针指的数一定是小于标记的数的,这时将该数,与hi所对应的数,交换。
至此,一次排序结束。
**这里要注意:**要先右指针先动然后左指针再动。
下面为代码:
public int partition(int[] arr, int lo, int hi, int k){
int i = lo, j = hi;
int Part = arr[lo];
while(true){
while(j > lo && arr[--j] >= Part);
while(i < hi - 1 && arr[++i] <= Part);
if(i >= j) break; // 这里注意下在结束时不要再进行交换了
swap(arr, i, j);
} // 这里便是排序
swap(arr, lo, j);
if(j > k)
partition(arr, lo, j, k);
else if(j < k)
partition(arr, j + 1, hi, k); // 这里为分治
return j;
}
第二种:快慢指针
将标记值放在6的位置,快慢指针都从1开始。
快指针 f 一直向右遍历整个数组,如果遇到有比标记值小的,就把这个值与慢指针的数进行交换,同时慢指针向前移动一位。
当遍历结束,将标记数与慢指针的数进行交换,此时,慢指针前的所有数都比标记值小,如此完成排序。
分治思想与上面相同
注:这里举的例子是最小的K个数;两种实现方式的目的相同,都是将标记值左边的值都小于它,右边的都大于它。
第二种:维持最大堆:时间复杂度O(NlogK)
算法逻辑:堆的总量为K。当堆不满时,将数据放到堆中,同时更新堆,要保持根的值是最大的。当堆满时,再来的数据,与根进行比较,如果小于根就将根的值移除,同时把输入的值放到堆中,并更新堆。如果大于,直接舍弃。
因为java中有这个堆的类,实现起来偏简单,这里就不深入展开了
超级重要的两种算法比较:
这个问题也经常被问到,这里就来解释下
1、因为快选会破坏原有的数组结构,而大根堆不对原数组进行操作。所以当面试官要求不破坏原数组的情况下,不用怀疑,直接大根堆。
2、数据量:因为快选是把所有的数据放到内存中,然后再进行操作。这样势必会带来一个问题,就是当数据量过大的时候,内存爆了,这个就很难受。而大根堆,就维持这么大个堆,其他的不用管。