经典面试题无序数组中,求第K大的数(堆荷兰国旗问题bfprt算法)
Posted 飞人01_01
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了经典面试题无序数组中,求第K大的数(堆荷兰国旗问题bfprt算法)相关的知识,希望对你有一定的参考价值。
今天来看一到算法题!经典面试题了,将从时间复杂度一般的解法,再到最优解!!!
题目:查找一个无序数组中,第K大的数。LeetCode链接
解法一、堆
分析:既然是求第K大的数,那么很显然,可以使用TOPK问题的角度来解题。只需建一个小根堆,堆的大小就是K,遍历一遍数组:
- 如果此时堆的大小<K,直接往里面放数据。
- 如果此时堆的大小>=K,那么只需比较堆顶与arr[i]的大小,如果堆顶的元素小于arr[i],就弹出堆顶,再放入arr[i]即可。
public int findKthLargest(int[] nums, int k)
if (nums == null || nums.length == 0 || k <= 0 || k > nums.length)
return -1;
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
int size = 0; //堆的大小
for (int i = 0; i < nums.length; i++)
if (size < k) //堆的大小 < K
size++;
minHeap.add(nums[i]);
else //堆的大小 >= K
if (minHeap.peek() < nums[i])
minHeap.poll();
minHeap.add(nums[i]); //放入比较大的元素
return minHeap.poll();
以上这种解法,因为整体遍历了一遍数组,是O(N),而遍历的每一个数,插入堆中最坏的情况就是O(logK),整体时间复杂度O(N*logK),空间复杂度O(K)这样的效率,还是达不到面试官的要求。我们接着看下一解法。
解法二、改进“荷兰国旗问题”
我们在之前学快速排序的时候,说到过一个优化,就是荷兰国旗问题优化。当时说的是根据一个基准值,将整个区域划分为大于区、小于区和等于区,此时这里也是一样的。我们既然要找第K大的数,对应在数组中,那就是下标为K-1的数据。
说到这里,我想你可能就理解我的意思了。
那就是,根据划分后的区域,小于区、等于区和大于区,判断K-1,是在这三个区域的哪一块区域内:
- K-1在等于区范围内,那么直接返回等于区的数据即可。
- K-1在大于区范围内,那么递归调用函数,再次进行划分即可。
- K-1在小于区范围内,也是递归调用小于区的范围即可。
如图:
//主方法
public int findKthLargest(int[] nums, int k)
if (nums == null || nums.length == 0 || k <= 0 || k > nums.length)
return -1;
//因为process函数求的是第K小的数,而题目是要求第K大的数
//所以此时调用的是计算第length-k小的数
return process(nums, 0, nums.length - 1, nums.length - k);
private int process(int[] nums, int L, int R, int index)
if (L == R)
return nums[L];
int pivot = L + (int)(Math.random() * (R - L)); //生成随机值
int[] mid = partition(nums, L, R, nums[pivot]); //划分范围
if (index >= mid[0] && index <= mid[1]) //index在等于区
return nums[index];
else if (index < mid[0]) //index在小于区
return process(nums, L, mid[0] - 1, index);
else //index在大于区
return process(nums, mid[1] + 1, R, index);
//荷兰国旗问题划分
private int[] partition(int[] arr, int L, int R, int pivot)
int less = L - 1;
int more = R + 1;
int index = L;
while (index < more)
if(arr[index] == pivot)
index++;
else if (arr[index] > pivot)
swap(arr, index, --more);
else
swap(arr, index++, ++less);
return new int[]less + 1, more - 1;
//交换数据
private void swap(int[] arr, int left, int right)
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
特别需要注意的就是在调用process函数的时候,因为这个函数是计算的是求第K小的数,而题目是要求第K大的数。这里在调用的时候,其实要转个弯,求的是第length-K小的数。
调用一次partition函数,时间复杂度是O(N),而在基准值的选取时,是随机产生的,因为基准值的选取很重要,只有基准值选在数组的中间位置,才能使这个算法达到最优的效果。但是在数学证明中,这个算法还是收敛于O(N)的水平。时间复杂度O(N),空间复杂度可以做到O(1)。将递归调用,改为迭代就可以做到空间复杂度O(1)。
解法三、bfprt算法
这个算法呢,是由五位大佬想出来的,也就是这五位大佬姓名的首字母组成的这个算法名。
这个算法,也是用于解决这个数组中第K大的数值的。也是能够做到时间复杂度O(N)的水平。因为上文中解法二,是一个概念性的基准值,在运气不好的情况下,那个算法的效率可能不是很理想。
bfprt算法,解决的就是如何选取基准值。其余的部分,还是跟解法二一样,基准值选出来之后,还是调用partition函数即可。
那么到底是如何选取基准值的呢?我先将步骤说出来,再一一讲解:
- 将整个数组,从左到右划分小组,5个数为一组。如果数组末尾不够5个数的,单独为一组;
- 将划分出来的小组,进行排序;(切记,是这小组中的5个数排序,不是整个数组)
- 挑选出排好序的小组中的中位数,组成一个新的数组;
- 基准值就是,新数组的排序后的中位数。
可能有点饶,看图就清楚了:
如果数组末尾不够5个数的,中间值就是上中位数,比如只有4个数时,返回第2个数,这就是上中位数。
最下面的红圆圈就是挑选出来的基准值。那么有人肯定会说,费这么大的劲挑选这个基准值,怎么说明他的重要性呢???来看如下分析:
通过上图的分析,我们就可以得出挑选出来的基准值d,d肯定是<=3N/10的。
因为整个代码会划分为三个区域,小于区、等于区和大于区。我们现在需要估计,小于区的数据最多有多少个?反推就是计算 大于区和等于区的数据最少有多少个?根据上图的分析,大于区和等于区最少有3N/10。
再者,每5个数据为一个小组,进行排序的时候,选择任意的排序方法都无所谓,应该一组是有5个数据,所以一个组排序的时间复杂度是O(1),而总共有N/5个小组。
再加上partition函数的时间复杂度是O(N)。
所以整体的时间复杂度如下:
//主方法
public int findKthLargest(int[] nums, int k)
if (nums == null || nums.length == 0 || k <= 0 || k > nums.length)
return -1;
//bfprt求的是第K小的数,而题目求的是第K大的数
return bfprt(nums, 0, nums.length - 1, nums.length - k );
private int bfprt(int[] arr, int L, int R, int index)
if (L == R) //只有一个数的时候
return arr[L];
//获取基准值-小组内排序
int pivot = medianOfMedians(arr, L, R);
int[] mid = partition(arr, L, R, pivot); //划分区间
if (index >= mid[0] && index <= mid[1]) //index在等于区内
return arr[index];
else if (index < mid[0]) //进入小于区
return bfprt(arr, L, mid[0] - 1, index);
else //进入大于区
return bfprt(arr, mid[1] + 1, R, index);
//5个数一组,并且排序,挑选出中位数,组成新数组
private int medianOfMedians(int[] arr, int L, int R)
int size = R - L + 1;
int offset = size % 5 == 0? 0 : 1; //数组末尾够不够5个数
int[] tmp = new int[size / 5 + offset];
for (int i = 0; i < tmp.length; i++)
int start = L + i * 5; //每个小组的开始下标
tmp[i] = getMedianNum(arr, start, Math.min(start + 4, R));//闭区间
//求新数组的中位数,再次调用bfprt函数即可
return bfprt(tmp, 0, tmp.length - 1, tmp.length / 2);//求tmp的中位数
private int getMedianNum(int[] arr, int L, int R)
selectSort(arr, L, R); //闭区间。小组内排序
return arr[(R + L) / 2]; //返回中间值
//选择排序
private void selectSort(int[] arr, int left, int right)
for (int i = left; i <= right - 1; i++)
int min = i;
for (int j = i + 1; j <= right; j++)
if (arr[j] < arr[min])
min = j;
if (min != i)
swap(arr, i, min);
private void swap(int[] arr, int left, int right)
int tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
解法二,已经是足够优秀的了。但是现在的算法难度越来越高,可能解法二不能够满足面试官的胃口了,那么此时你就可以说一说,这道题还有一个bfprt算法可以解。
好啦,以上全部就是本期文章的全部内容啦!我们下期见吧!!!
以上是关于经典面试题无序数组中,求第K大的数(堆荷兰国旗问题bfprt算法)的主要内容,如果未能解决你的问题,请参考以下文章