今天来看一到算法题!经典面试题了,将从时间复杂度一般的解法,再到最优解!!!
文章目录题目:查找一个无序数组中,第K大的数。LeetCode链接
- 解法一、堆
- 解法二、改进“荷兰国旗问题”
- 解法三、bfprt算法
分析:既然是求第K大的数,那么很显然,可以使用TOPK问题的角度来解题。只需建一个小根堆,堆的大小就是K,遍历一遍数组:
- 如果此时堆的大小
- 如果此时堆的大小>=K,那么只需比较堆顶与arr[i]的大小,如果堆顶的元素小于arr[i],就d出堆顶,再放入arr[i]即可。
public int findKthLargest(int[] nums, int k) { if (nums == null || nums.length == 0 || k <= 0 || k > nums.length) { return -1; } PriorityQueueminHeap = 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算法可以解。
好啦,以上全部就是本期文章的全部内容啦!我们下期见吧!!!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)