BFPRT算法

一、先来看一个问题

在一个乱序的数组中,寻找第k个小的值?

很多人第一种解法,用大顶堆,然后poll第k个就是答案了,但是时间复杂度是O(nlogn),有没有O(n)的算法呢?答案是肯定的,就是我们的BFPRT算法

二、铺垫

这里我们需要大家知道荷兰国旗问题的解法,也就是选定一个基值,比它小的放在左边,比它大的放在右边,相等的放在中间,其实就是快速排序的partition过程。数组{5,7,12,4,2,7,2,5,6,0,1,2},假设选定基值是6,那么数组表示

这样实际就是一个荷兰国旗,这个时候我们保存中间6的左边界l和右边界r,判断 k-1 是否在l 和 r是否在中间,如果在就说明是6了,等于中间的值。如果不是,判断大小选择左边数组继续递归还是右边的数组继续递归。这样就可以一直找到这个值了。这种时间复杂度O(n),这种是基于概率的时间复杂度的一个期望。

考虑一种极端情况,如果每一次找出的值,都是没有右边只有左边,假设样本n,T(n)= T(n - 1) + n。那么时间复杂度就是O(n2)。实际上就失去了意义,说来说去还是这个基准值6怎么来找?如果我们能够保证每次找到基准值的左右两边数组的长度近似相等,那么就可以达到O(n),同时这种方法是稳定的,不是基于概率的,我们的BFTRP算法就是用来找到这个基准值的。

三、上菜

1、小小的分析

比如现在有:arr = [1,2,4,1,2,5,7,3,21,7,3,1,6,,0,6,3,1,34,65,62,2,1,5,7,10],这样一个数组。

我没分为五个一组(如果剩余的数不够5,那么单独成一组):

对每一个数组进行排序,每个数组里面有5个数,是常数长度,那么时间复杂度O(1),因为有n/5,所以时间复杂度还是O(n)。注意这个排序只跟小组内部有关,跟数组之间没有关系。排序后就是这个熊样子

然后取中位数,形成一个新数组:

新数组[2,7,3,34,5]。然后我们还是用这种方式以5个为一组进BFTRP,只不过现在寻找的k变成了新数组长度的中间位置数,很好理解,现在要找中位数,那么就是第中间位置小的数,就是中位数了。在这个例子中,我们找到中位数7,然后就以7为基准值,比它小的放左边........。然后看要找的k是否在中间区域,如果在,直接返回7,如果不在向左边或者右边递归。

我们来看一下,为什么这个能够达到O(n),因为比如刚开始数组为n,那么五个为一组就有n/5个数组,那么这些个数组会拿出中位数组成new int[n/5]这么大的数组,有n/5个数,这些个数的中位数中大于等于该中位数的有n/10个,就是n/(5*2),n/10,这些数,在原来的数组中有3个大于等于中位数的数,所以就是至少有3n/10个数大于你要找到的中位数,那么也就是最多有7n/10个数小于中位数的,所以可以保证不会出现上面极端情况。

时间复杂度 T(N) = T(N/5) + T(7N /10) + O(N)。最后通过计算(算法导论第九章)时间复杂度O(n)。

实际上最重要的就是找到基准值,一个中位数

2、代码

   public static void main(String[] args) {
        int[] arr = { 6, 9, 1, 3, 1, 2, 2, 5, 6, 1, 3, 5, 9, 7, 2, 5, 6, 1, 9 };
        System.out.println(getMinKthByBFPRT(arr,10));

    } 
   /**
     * 获得第k小的数
     */
    public static int getMinKthByBFPRT(int[] arr, int K) {
        return select(arr, 0, copyArr.length - 1, K - 1);
    }

main函数调用这个方法,传入数组,开始位置、结束位置,以及要查找的下标。

    /**
     * bftrp 寻找中位数。
     */
    public static int select(int[] arr, int begin, int end, int i) {
        if (begin == end) {
            return arr[begin];
        }
        int pivot = medianOfMedians(arr, begin, end);
        int[] pivotRange = partition(arr, begin, end, pivot);
        if (i >= pivotRange[0] && i <= pivotRange[1]) {
            return arr[i];
        } else if (i < pivotRange[0]) {
            return select(arr, begin, pivotRange[0] - 1, i);
        } else {
            return select(arr, pivotRange[1] + 1, end, i);
        }
    }

select方法就是BFTRP算法核心内容

(1)如果寻找的数组开始和结束位置一样,那么直接返回

(2)medianOfMedians获取中位数,也就是我们的基准值。

(3)以这个基准值整理数组,比它小的在左边,比它大的在右边,相等的在中间。

(4)pivotRange[0]表示中间基准值的左边界,pivotRange[1]表示中间基准值的右边界,如果在这之内直接返回,否则向左边或者右边递归调用。

   /**
     * 中位数数组作为划分值,求中位数数组的中位数
     */
    public static int medianOfMedians(int[] arr, int begin, int end) {
        int num = end - begin + 1;
        //以五个数为一组
        int offset = num % 5 == 0 ? 0 : 1;
        //中位数数组
        int[] mArr = new int[num / 5 + offset];
        //填充中位数数组
        for (int i = 0; i < mArr.length; i++) {
            int beginI = begin + i * 5;
            int endI = beginI + 4;
            mArr[i] = getMedian(arr, beginI, Math.min(end, endI));
        }
        //递归获取中位数
        return select(mArr, 0, mArr.length - 1, mArr.length / 2);
    }

获取中位数,就是上边说的以5个为一组排序,然后或者每组的中位数,然后再次调用BFTRP算法,只不过查找的中间变成了数组的中间位置,这个值就是中位数!!!最后是可以返回到上面的。然后操作。

    /**
     * 获取中位数
     */
    public static int getMedian(int[] arr, int begin, int end) {
        insertionSort(arr, begin, end);
        int sum = end + begin;
        int mid = (sum / 2) + (sum % 2);
        return arr[mid];
    }

    /**
     * 排序小数组
     */
    public static void insertionSort(int[] arr, int begin, int end) {
        for (int i = begin + 1; i != end + 1; i++) {
            for (int j = i; j != begin; j--) {
                if (arr[j - 1] > arr[j]) {
                    swap(arr, j - 1, j);
                } else {
                    break;
                }
            }
        }
    }

    public static void swap(int[] arr, int index1, int index2) {
        int tmp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = tmp;
    }

排序获取中位数代码!!

    /**
     * 以pivotValue为中心,比他小的放左边,比他大的放右边
     * 返回一个两个元素的数组,0位置是中间数的左边,1位置是中间数的右边
     */
    public static int[] partition(int[] arr, int begin, int end, int pivotValue) {
        int small = begin - 1;
        int cur = begin;
        int big = end + 1;
        while (cur != big) {
            if (arr[cur] < pivotValue) {
                swap(arr, ++small, cur++);
            } else if (arr[cur] > pivotValue) {
                swap(arr, cur, --big);
            } else {
                cur++;
            }
        }
        int[] range = new int[2];
        range[0] = small + 1;
        range[1] = big - 1;
        return range;
    }

partition其实就是荷兰国旗那段代码。

最后就得到答案了。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值