算法笔记——左神进阶(2)BFPRT算法:TOP-K问题(查找第k小的数)+ 窗口内最大值的求解

一、BFPRT算法

在一个数组中找出第k大的数

1、暴力解法:先排序,再找
2、快速排序:参考荷兰国旗问题,随机选数,分为左中右三个部分,然后按数量选择左边或右边区域,继续按荷兰国旗问题分三块,直到取到序号k在中间等于区域,则此时的中间数就是第k大的数。
3、BFPRT算法:

BFPRT算法跟快排的算法只有在选取划分值的情况上不同,其他全部一样。
【步骤】

  1. 分组(假设每五个一组,最后剩余的不到五个的一组) O(1)
  2. 分组之后每个小组之内排序,跨组不排序,五个数排序,总共需要划分的时间复杂度为O(N)。
  3. 将每个组的中位数拿出,构成新的数组,此时新数组长度为N/5(最后不到五个的可以拿上中位数,也可以拿下中位数) O(N)
  4. 调用BFPRT算法,此时递归过程中不再寻找k项,而是选择中间的中位数 T(N/5)
  5. 下面就是利用上述num,进行荷兰国旗问题排序 O(N)
  6. 选择走左边或者走右边
    之所以要这样来选出中位数作为基准值,是因为这样分组后的左右规模就是确定了的,此时估计至少有多少个数比k更大,则确定了左右部分的最大规模。

【代码】

//用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;
}


二、窗口

窗口的概念就是一个由左右边界划分的一个区域,窗口从左向右滑动,右边进数,左边出数。窗口长度可以变也可以不变,具体问题具体分析。

题目1:窗口内最大值的求解

双端队列,代价都是O(1)。

双端队列中每次保存两个值,当前的数值和当前的位置信息。
【加数的逻辑】:

  1. 每次小的值从右边进入双端队列,整个队列形成从大到小排列的顺序。
  2. 如果出现新的数值大于最右边的小数值,则将小的数弹出,直到放得下新的数值。
  3. 如果出现相等的数值,则将先前的数弹出,将新的数保存 【注意】这里弹出的是数,重点是数的位置而不是数的大小,因为大小可以用arr通过位置去找。

【减数的逻辑】:

  1. L移动的时候,分析当前最大值所在的位置信息是否过期,如果过期则弹出,没过期则显示当前的最大值。

加数的逻辑中的原理解释:每次增加新的值的时候,如果新增的值比当前双端队列中保存的值小,则需要保留,因为窗口滑动过去之前的较大值之后后面现在的较小的值还有可能变成最大值。当新来了一个较大的值,比队列中的一些值大或相等,则可以直接将那些值删掉,因为接下来那些值存在的时候,新来的值一定存在,并且比那些值大,所以可以直接删掉。对于双端队列,可以只保存下角标,然后从原始数组中去读取数据值。

【例题】
在这里插入图片描述
复杂度O(N)

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;
}

题目2:最大值减去最小值小于或等于num的子数组数量

给定数组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达标,则任何其中的子数组也全部达标。同理,如果一个子数组不达标,则将其任意向外扩,则那些数组也不达标,利用这样的性质用双端队列来求解。

【步骤】

  1. L停留在0位置,如果一直达标,则将R向右扩,直到扩到某个位置X,再往后扩一个,则不达标。
  2. 窗口内最大值的更新结构和窗口内的最小值更新结构可以很简单获得最大和最小值,从而进行计算。
  3. 以0开头的子数组达标数量则计算了出来,一共有x+1长度的数组达标,他的所有子数组全部达标。
  4. 将L来到1位置,此时将窗口最大值和最小值更新,此时R可能仍然可以向外扩,此时右可以得到以1位置开头的所有可行的数组。
  5. 因为没有回退,一直是当前位置向后走,总的复杂度为O(N)。
    【代码】
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;

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
BFPRT算法是一种解决Top-K问题算法,其中K为要找的第K大的元素。该算法的时间复杂度为O(n)。 BFPRT算法的基本思想是通过划分算法来找到中位,然后根据中位组分成两部分,只需要在一部分中查找Top-K,另一部分不用查找。 以下是BFPRT算法的c语言实现: ```c #include <stdio.h> #include <stdlib.h> #define SWAP(a, b) {int temp = a; a = b; b = temp;} // 求中位 int median(int arr[], int left, int right) { int center = (left + right) / 2; if (arr[left] > arr[center]) SWAP(arr[left], arr[center]) if (arr[left] > arr[right]) SWAP(arr[left], arr[right]) if (arr[center] > arr[right]) SWAP(arr[center], arr[right]) SWAP(arr[center], arr[right - 1]) return arr[right - 1]; } // 划分函 int partition(int arr[], int left, int right, int pivot) { int i, j; for (i = left, j = right - 2;;) { while (arr[++i] < pivot); while (arr[--j] > pivot); if (i < j) SWAP(arr[i], arr[j]) else break; } SWAP(arr[i], arr[right - 1]) return i; } // BFPRT算法 int bfprt(int arr[], int left, int right, int k) { if (left + 1 == right) return arr[left]; int pivot = median(arr, left, right); int mid = partition(arr, left, right, pivot); int num = mid - left + 1; if (k == num) return arr[mid]; else if (k < num) return bfprt(arr, left, mid, k); else return bfprt(arr, mid + 1, right, k - num); } int main() { int arr[] = {3, 4, 2, 1, 5, 6, 8, 7, 9}; int len = sizeof(arr) / sizeof(int); int k = 5; int kth = bfprt(arr, 0, len, k); printf("The %dth number is: %d\n", k, kth); return 0; } ``` 该算法的核心是BFPRT划分算法,该算法是一个递归算法,每次递归的时间复杂度为O(n),因此总的时间复杂度为O(nlogn)。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值