BFPRT算法跟快排的算法只有在选取划分值的情况上不同,其他全部一样。
【步骤】
【代码】
//用BFPRT方法得到第k个最小的值
public static int getMinKthByBFPRT(int[] arr,int k ){
int[] copyArr = copyArray(arr);
//得到数组中第k-1位置上的值就是第k小的值
return bfprt(copyArr,0,copyArr.length-1,K-1);
}
//bfprt方法主体,在bigin和end范围上求第i小的数
public static int bfprt(int[] arr,int begin,int end,int i){
if(begin == end){
return arr[begin];
}
//求中位数的中位数
int pivot = medianOfMedians(arr,begin,end);
//求完第二轮的中位数之后就开始进行划分
int[] privotRange = partition(arr,begin,end,pivot);
//正好i位置等于相等部分则返回
if(i>= pivotRange[0] && i <= pivotRange[1]){
return arr[i];
//i小于排序起始位置的情况,走左边
}else if(i<pivotRange[0]){
return bgprt(arr,begin,pivotRange[0]-1,i);
}else{
//i大于终止位置的情况,走右边
return bfprt(arr,pivotRange[1]+1,end,i);
}
}
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+offest];
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 bfprt(mArr,0,mArr.length-1,mArr.length/2);
}
//partition是实现荷兰国旗问题,将大于小于等于三类划分开
public static int[] partition(int[] arr,int begin,int end,int privotValue){
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++;
}
}
//range返回两个值,第一个是起始位置,第二个是排序的终止位置
int[] range = new int[2];
range[0] = samll+1;
range[1] = big-1;
return range;
}
窗口的概念就是一个由左右边界划分的一个区域,窗口从左向右滑动,右边进数,左边出数。窗口长度可以变也可以不变,具体问题具体分析。
双端队列,代价都是O(1)。
双端队列中每次保存两个值,当前的数值和当前的位置信息。
【加数的逻辑】:
【减数的逻辑】:
加数的逻辑中的原理解释:每次增加新的值的时候,如果新增的值比当前双端队列中保存的值小,则需要保留,因为窗口滑动过去之前的较大值之后后面现在的较小的值还有可能变成最大值。当新来了一个较大的值,比队列中的一些值大或相等,则可以直接将那些值删掉,因为接下来那些值存在的时候,新来的值一定存在,并且比那些值大,所以可以直接删掉。对于双端队列,可以只保存下角标,然后从原始数组中去读取数据值。
public static int[] getMaxWindow(int[] arr,int w){
if(arr == null || w<1 || arr.length <w){
return null;
}
//LinkedList就是双向链表
LinkedList<Integer> qmax = new LinkedList<Integer>();
int[] res = new int[arr.length-w+1];
int index = 0;
for(int i = 0;i<arr.length;i++){
//双端队列不为空,并且尾部的数值小于新加入的数值,则弹出尾部下标
while(!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[i]){
qmax.pollLast();
}
qmax.addLast(i);
//判断首位上的数据是否过期,进行下标和i-w的判断,过期就弹出下标,不过期就继续下面的操作
//窗口如果没有形成完全,则不会弹出。此处针对不同题目需要进行修改,看窗口是否要全完全显示之后才开始计算最大值
if(qmax.peekFirst() == i-w){
qmax.pollFirst(); //弹出过期头结点
}
//如果i还在窗口之中,则返回第一个数值。
if(i>=w-1){
res[index++] = arr[qmax.peekFirst()];
}
}
return res;
}
给定数组arr和整数num,返回一段数组中最大值减去最小值小于num的数目,要求:数组长度为N,请实现时间复杂度为O(N)的解法。
【注意】子数组是有序的,不是子序列。所有子数组的数目等于等差数列n(n-1)/2。先给出一种暴力解法,暴力解法中,其实就是首先两个for循环遍历所有的子数组,然后在所有的过程中进行一个最大减最小。复杂度O(N^3)
public static int getNum1(int[] arr,int num){
int res = 0;
for(int start = 0;start <arr.length;start++){
for(int end = start;end<arr.length;end++){
if(isValid(arr,start,end,num){
res++;
}
}
}
return res;
}
public start boolean isvalid(int[] arr,int start,int end,int num){
int max = Integer.MAX_VALUE;
int min = Integer.MIN_VALUE;
for(int i = start;i<=end;i++){
max = Math.max(max,arr[i]);
min = Math.min(min,arr[i]);
}
return max-min <= num;
}
【思考】复杂度太高,所以需要省略掉一些重复无效操作。考虑到,如果中间某个数组从L——R达标,则任何其中的子数组也全部达标。同理,如果一个子数组不达标,则将其任意向外扩,则那些数组也不达标,利用这样的性质用双端队列来求解。
【步骤】
public static int getNum(int[] arr,int num){
if(arr == null || arr.length == 0){
return 0;
}
//窗口内最大值 和 窗口内最小值
LinkedList<Integer> qmin = new LinkedList<Integer>();
LinkedList<Integer> qmax = new LinkedList<Integer>();
int i = 0;//i为start,j为end
int j = 0;
int res = 0;
//整个while循环是将R向右不断扩展
while(i<arr.length){
while(j<arr.length){ //左边界确定的情况下,右边界尽可能扩展
while(!qmin.isEmpty() && arr[qmin.peeklast()] >= arr[j]){
qmin.polllast(); //最小值结构更新
}
qmin.addLast(j);
while(!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[j]){
qmax.pollLast();
}
qmax.addLast();
if(arr[qmax.getFirst()]- arr[qmin.getFirst()] > num){
break; //不达标就跳出
}
j++;
}
//判断最大最小值是否下标过期,即需要弹出
if(qmin.peekFirst() == i){
qmin.pollFirst;
}
if(qmax.peekFirst() == i){
qmax.pollFirst;
}
//一次性获取完以i开头的所有满足的数组
res += j-i;
i++;
}
return res;