BFPRT->求无序数组中第K小的数:要求时间复杂度O(n):两种方法!

昨天和好友 @覃会程 仔细探讨了关于在无序数组中 求第K小或者第K大的数这道题,记下来->直呼妙啊!

  1. 需要奠定的知识点
    1. 对于中间的数provit的选取,为将最坏的情况概率降到最低,这块将使用随机的方法。
    2. 在快排的基础上进行,又不与快排相似,正是这个不相似的点,让时间复杂度由O(NlogN)收敛到 O(N)。
      1. 区别:
        1. 快排需要即向左,又向右
        2. 而此方法只需往一边走,另一边就被排除掉了,由此缩小范围
    3. 以及对partition函数的详述:将数组分为:等于,大于,小于三段。
  2. 对于partition方法的解释:
    1.  

话不多说,上代码--->看看较为简单的路数。BFPRT和简单路数的区别在于取provit这个数:可谓是精雕细琢,十分严谨!整体来说:大差不差。

package LiKou.BFPRT;

public class SimpleGetMinKTH {
    public static void main(String[] args) {
        int[] arr = {4, 2, 8, 1, 5};
        int i = getKTH(arr, 0, arr.length - 1, 3); // 要进行查找的数组 ,起点 终点  求第几小的数
        System.out.println(i);
    }

    /**
     * @param arr   查找的数组
     * @param left  起点
     * @param right 终点
     * @param th    第几小
     * @return
     */
    private static int getKTH(int[] arr, int left, int right, int th) {
        //base case
        if (left == right) {
            return arr[left];
        }
        //寻找一个随机的坐标作为其中间值
        int provit = arr[left + (int) (Math.random() * (right - left + 1))];
        //将数组分为小于 等于 大于三段    最终只返回一个范围
        int[] range = partition(arr, left, right, provit);
        //如果在范围内直接返回,说明命中
        if (th - 1 >= range[0] && th - 1 <= range[1]) {
            return arr[range[0]];
        } else if (th - 1 < range[0]) { //说明没有命中,在小于的区间里,因此重新进行划分的数组坐标范围为  起点:不变,终点:范围内最小的索引-1
            return getKTH(arr, left, range[0] - 1, th);
        } else { //反之
            return getKTH(arr, range[1] + 1, right, th);
        }

    }

    //分割函数
    private static int[] partition(int[] arr, int left, int right, int provit) {
        int l = left - 1; //记录小于的已经处理的坐标
        int r = right + 1;//记录大于的已经处理的坐标
        int cur = left; //轮询
        while (cur < r) {//不要到已经确定的大于的索引了
            if (arr[cur] < provit) {
                swap(arr, cur++, ++l);//小于provit的话交换,放未处理的到左边使之变为已经处理的,因为最左边已经处理过了,cur然后进行下一个
            } else if (arr[cur] > provit) {
                swap(arr, cur, --r); //大于小于provit的话交换,放到右边,因为最右边已经处理过了,cur交换后的值和provit的大小关系不确定,因此需要接着比较
            } else {
                cur++;//等于的话直接完后推,没必要交换
            }
        }
        return new int[]{l + 1, r - 1}; //考虑到可能由多个和provit值一样的,因此两个值可能一样,可能不一样
    }

    //交换
    private static void swap(int[] arr, int l, int r) {
        int temp = arr[r];
        arr[r] = arr[l];
        arr[l] = temp;
    }

}
  1. 再来看看真正的BFPRT算法
    1. 核心思路:对分割值provit严谨的选用
    2. 选用步骤:
      1. 以5个值为1组对数组进行分组,剩余不足5个或者刚好5个为1组
      2. 将所有组进行排序:即组内有序,各组之间无序
      3. 取所有组对中位数,到最后一个组的时候(如果不为5个,奇数个取中间的,偶数个取上中位数或者下中位数),总之只能取一个。
      4. 对每个组的中位数形成一个新的中位数数组,然后用这个新的中位数数组重新进行选举,最终返回中位数数组中的中位数。此处数据规模减小到n/5
      5. 最后返回一个最终的provit
      6. 下面的分割步骤就和上面的一样了。
  2. 看代码
  3. package LiKou.BFPRT;
    
    public class MyBFPRT {
    
        public static void main(String[] args) {
            int[] arr = {4, 2, 8, 1, 5};
            int i = bfprt(arr, 0, arr.length - 1, 3); 
            System.out.println(i);
        }
        // arr[L..R]
        public static int bfprt(int[] arr, int L, int R, int index) {
            if (L == R) { //剩一个数的时候,直接返回
                return arr[L];
            }
            
            
            int pivot = medianOfMedians(arr, L, R);  //***关键点***
    
            int[] range = partition(arr, L, R, pivot);
            if (index >= range[0] && index <= range[1]) {
                return arr[index];
            } else if (index < range[0]) {
                return bfprt(arr, L, range[0] - 1, index);
            } else {
                return bfprt(arr, range[1] + 1, R, index);
            }
        }
    
        // arr[L...R]  五个数一组
        // 每个小组内部排序
        // 每个小组中位数领出来,组成marr
        // marr中的中位数,返回
        public static int medianOfMedians(int[] arr, int L, int R) {
            int size = R - L + 1;//总大小
            int offset = size % 5 == 0 ? 0 : 1; // 看是否会多1个
            int[] mArr = new int[size / 5 + offset]; //创建一个新的数组
            for (int team = 0; team < mArr.length; team++) { //遍历新的数组
                int teamFirst = L + team * 5;//用来控制范围
                // L ... L + 4
                // L +5 ... L +9
                // L +10....L+14
                mArr[team] = getMedian(arr, teamFirst, Math.min(R, teamFirst + 4));//排序,返回范围内的中位数
            }
            // marr中,找到中位数
            // marr(0, marr.len - 1,  mArr.length / 2 )
            return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2);//用bfprt算法取得坐标为(长度-1)/2位置的数
        }
    
        //排序并返回范围内的中位数
        public static int getMedian(int[] arr, int L, int R) {
            insertionSort(arr, L, R);
            return arr[(L + R) / 2];
        }
        //排序
        public static void insertionSort(int[] arr, int L, int R) {
            for (int i = L + 1; i <= R; i++) {
                for (int j = i - 1; j >= L && arr[j] > arr[j + 1]; j--) {
                    swap(arr, j, j + 1);
                }
            }
        }
    
        public static void swap(int[] arr, int i1, int i2) {
            int tmp = arr[i1];
            arr[i1] = arr[i2];
            arr[i2] = tmp;
        }
    
        public static int[] partition(int[] arr, int L, int R, int pivot) {
            int less = L - 1; //-1
            int more = R + 1; //数组的长度
            int cur = L; // 0
            while (cur < more) { //如果没越界
                if (arr[cur] < pivot) { //左边的小于中间的
                    swap(arr, ++less, cur++);
                } else if (arr[cur] > pivot) {//左边的大于中间的,当前坐标和最右边进行交换
                    swap(arr, cur, --more);
                } else { //等于的话直接跳过
                    cur++;
                }
            }
            return new int[]{less + 1, more - 1};
        }
    
    }
    

    大致思路就是这样,看不懂的童鞋 debug 一下就会拨开云雾,加油!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值