经典面试题无序数组中,求第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函数即可。

那么到底是如何选取基准值的呢?我先将步骤说出来,再一一讲解:

  1. 将整个数组,从左到右划分小组,5个数为一组。如果数组末尾不够5个数的,单独为一组;
  2. 将划分出来的小组,进行排序;(切记,是这小组中的5个数排序,不是整个数组)
  3. 挑选出排好序的小组中的中位数,组成一个新的数组;
  4. 基准值就是,新数组的排序后的中位数。

可能有点饶,看图就清楚了:

如果数组末尾不够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算法)的主要内容,如果未能解决你的问题,请参考以下文章

经典面试题无序数组中,求第K大的数(堆荷兰国旗问题bfprt算法)

2020-03-02:在无序数组中,如何求第K小的数?

无序数组求第K大/第K小的数

基于快速排序方法改成求第k大的数

【python滴滴出行】整数无序数组求第K大数?

滑动窗口8:75. 颜色分类-经典荷兰国旗问题