一、bfprt算法 时间复杂度O(N)
在前面写过一篇排序算法快排算法中,我们优化版本 随机快排+荷兰国旗技巧优化 ,其中我们优化核心点就是把划分值定成随机等概率的l..r上的某个位置,减少了较坏情况的概率,
那么 bfprt算法,就是通过一个固定的逻辑算法过程,定义一个比较良好的划分值位置。不再是随机得到的一个位置。
二、在无序数组中求第K小的数
1)改写快排的方法(不用全部完成排序,只需对符合的一个区域 中心区、小于区、大于区 做一个递归排序)
2)bfprt算法
package class29;
import java.util.PriorityQueue;
/**
* 在无序数组中求第K小的数
*/
public class FindMinKth {
// 利用大根堆,时间复杂度O(N*logK)
public static int minKth1(int[] arr, int k) {
PriorityQueue<Integer> maxHeap = new PriorityQueue<>((a, b) -> (b-a));
//先把k个值入堆
for(int i = 0; i < k; i++){
maxHeap.add(arr[i]);
}
//接着从k位置开始 遍历到最后 进行比较
for(int i = k; i < arr.length; i++){
if(maxHeap.peek() > arr[i]){
//k位置往后 如果值小于堆顶 堆顶弹出 当前值i入堆
maxHeap.poll();
maxHeap.add(arr[i]);
}
}
return maxHeap.peek(); //最后堆顶就是第k小的值
}
// 改写快排,时间复杂度O(N)
// k >= 1
public static int minKth2(int[] array, int k) {
//数组深拷贝一个数组, 然后调用递归函数
int[] res = copyArray(array);
//调用递归函数 左右边界索引 k个索引对应的是k-1 第一小 也就是索引0
return process2(res,0,res.length-1,k-1);
}
public static int[] copyArray(int[] array){
int[] res = new int[array.length];
for(int i = 0; i < res.length; i++){
res[i] = array[i];
}
return res;
}
//递归函数
// arr 第k小的数
// process2(arr, 0, N-1, k-1)
// arr[L..R] 范围上,如果排序的话(不是真的去排序),找位于index的数
// index [L..R]
public static int process2(int[] arr, int l, int r, int index){
//base case: 当l==r 也就是只有一个值时 那么这个值就是其值了 因为范围内一定存在第k小的数
if(l == r) return arr[l];
//l < r,进行荷兰国旗 快速排序 这里我们的划分值pivot 通过随机获取数组上任意一个 以达到复杂度能来到O(N)
// 随机值 从 l 到 r l + [0,r-l]
int pivot = arr[l + (int) (Math.random()*(r-l+1))];
//调用荷兰国旗快排方法 返回当前划分值后 值为pivot的左右边界
int[] range = partition(arr,l,r,pivot);
//判断 如果k位置在 区间范围内 那么就表示当前位置值就是 第k小的值
if(index >= range[0] && index <= range[1]){
return arr[index];
}else if(index < range[0]){
//比左边界小 那么就递归左边范围值 0,l-1
return process2(arr,0,range[0]-1,index);
}else {
// 比右边界大
return process2(arr,range[1]+1,r,index);
}
}
//快排 只对符合的一边进行排序 荷兰国旗
public static int[] partition(int[] arr, int l, int r, int pivot){
//定义一开始小于区的右边界 大于区的左边界 两边界一开始属于不在数组索引内 当前位置在l上
int less = l-1;
int more = r+1;
int cur = l;
//遍历 从cur 开始 直到与右侧 大于区的左边界相遇就退出
while (cur < more){
if(arr[cur] < pivot) {
//当前cur位置值小于划分值 那么就与小于区右边界+1位置交换 然后cur++来到下个位置
swap(arr,++less,cur++);
}else if(arr[cur] > pivot){
//当前cur位置值大于划分值 那么就与大于区左边界-1位置交换 此时交换后右侧值来到cur 还没判断 所以cur不变 继续当前位置判断
swap(arr,--more,cur);
}else{
cur++; //如果是相等 那么就不用交换了 直接来到下个区间
}
}
return new int[]{less+1,more-1}; //最后返回中间pivot值的左边边界
}
public static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
// 利用bfprt算法,时间复杂度O(N)
public static int minKth3(int[] array, int k) {
int[] arr = copyArray(array); //深拷贝数组
return bfprt(arr,0,arr.length-1,k-1); //调用bfprt算法函数 传递数组 左右边界 第k小索引k-1
}
//bfprt算法 与前面minKth2方法差异在于 这个划分值 不是随机等概率获取。而且通过固定逻辑分组排序取中位数再取中位数得到的一个比较良好的划分值
public static int bfprt(int[] arr, int l, int r, int k){
//base case: 当前左右边界相等 说明区间仅有一个数 已知区域中存在第k小的值 索引当前这个位置就是所求值
if(l == r)return arr[l];
// L...R 每五个数一组
// 每一个小组内部排好序
// 小组的中位数组成新数组
// 这个新数组的中位数返回
int pivot = medianOfMedians(arr, l, r);
int[] range = partition(arr,l,r,pivot);
if(k >= range[0] && k <= range[1]){
return arr[k];
}else if(k < range[0]){
return bfprt(arr,0,range[0]-1,k);
}else{
return bfprt(arr,range[1]+1,r,k);
}
}
//arr[l....r] 五个数一组
// L...R 每五个数一组
// 每一个小组内部排好序
// 小组的中位数组成新数组
// 这个新数组的中位数返回
public static int medianOfMedians(int[] arr, int l, int r){
int size = r - l + 1; //区间范围的长度大小
//5个数一组 人为规定 这里会存在最后一组不足5个 那也需要算一组
//所以如果整除 flag就是0 不整除就是1 表示还需要这一组
int flag = size % 5 == 0 ? 0 : 1;
//定义一个新数组 mArr 存在每个小组的中位数
int[] mArr = new int[(size/5) + flag];
//每个组的中位数 需要先将每个小组内的排好序 然后取中位数得到 再依次填充到mArr
for(int i = 0; i < mArr.length; i++){
int iL = l + i * 5; //每一组的左边界
//i是每一个小组 每组5个数
//第一组 l,l+4
//第二组 l+5, l + 9
//第三组 l+10, l+14...
//每个小组的中位数 调用函数 该函数就是先排序 再返回中位数,
// 注意右边界 Math.min(r,iL + 4) 每一组都是左边界+4个凑5个 但最后一组可能不满5个 所以右边界就要取r 防止越界
mArr[i] = getMedian(arr,iL, Math.min(r,iL + 4));
}
//小组的中位数组成新数组mArr已经处理好 接着就是返回中位数
//那么就可以回到调用bfprt函数 来获取中位数了 就是前k小的数 中位数返回其对应索引 marr.length/2 如果是偶数个 两个中位数就取向上的一个 靠左的 也是marr.length/2索引位置
return bfprt(mArr,0,mArr.length-1,mArr.length/2);
}
//获取每个小组内的中位数
//先排序,个数少 直接用插入排序
public static int getMedian(int[] arr, int l, int r){
insertSort(arr,l,r);
//排序后 直接返回l,r区间的中位数
return arr[(l+r)/2];
}
public static void insertSort(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);
}
}
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) (Math.random() * maxSize) + 1];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * (maxValue + 1));
}
return arr;
}
public static void main(String[] args) {
int testTime = 1000000;
int maxSize = 100;
int maxValue = 100;
System.out.println("test begin");
for (int i = 0; i < testTime; i++) {
int[] arr = generateRandomArray(maxSize, maxValue);
int k = (int) (Math.random() * arr.length) + 1;
int ans1 = minKth1(arr, k);
int ans2 = minKth2(arr, k);
int ans3 = minKth3(arr, k);
if (ans1 != ans2 ||ans2 != ans3) {
System.out.println("Oops!");
}
}
System.out.println("test finish");
}
}
三、给定一个无序数组arr中,长度为N,给定一个正数k,返回top k个最大的数,不同时间复杂度三个方法
给定一个无序数组arr中,长度为N,给定一个正数k,返回top k个最大的数
不同时间复杂度三个方法:
1)O(N*logN)
2)O(N + K*logN)
3)O(n + k*logk)
package class29;
import java.util.Arrays;
/**
* 给定一个无序数组arr中,长度为N,给定一个正数k,返回top k个最大的数
*
* 不同时间复杂度三个方法:
*
* 1)O(N*logN)
* 2)O(N + K*logN)
* 3)O(n + k*logk)
*/
public class MaxTopK {
// 时间复杂度O(N*logN)
// 排序+收集
public static int[] maxTopK1(int[] arr, int k) {
//边界判断
if(arr == null || arr.length == 0)
return new int[0];
//判断k 如果超过了数组长度 那么就返回数组长度 避免越界异常
k = Math.min(arr.length,k);
//利用排序函数直接排序 默认是升序
Arrays.sort(arr);
//定义一个结果数组res 返回top k个最大数 需要从arr数组倒序的遍历
int[] res = new int[k];
for(int i =0,j = arr.length-1; i < k; i++,j--){
res[i] = arr[j];
}
return res;
}
// 方法二,时间复杂度O(N + K*logN)
// 解释:堆
public static int[] maxTopK2(int[] arr, int k) {
//边界判断
if(arr == null || arr.length == 0)
return new int[0];
//判断k 如果超过了数组长度 那么就返回数组长度 避免越界异常
int n = arr.length;
k = Math.min(n,k);
//heapify 自底向上构建大根堆 数组转成大根堆
// 从底向上建堆,时间复杂度O(N)
for(int i = n-1; i >=0; i--){
heapify(arr,i,n);
}
// 只把前K个数放在arr末尾,然后收集,O(K*logN)
//构建好大根堆 就开始排序,先将首个位置进行交换到底
int heapSize = n;
//先下沉交换堆顶最大值到heapsize-1索引位置 同时大小-1
swap(arr, 0, --heapSize);
int count = 1;
while(heapSize > 0 && count < k){
//接着只需判断k-1个 前面第一个堆顶已经排好在数组最后一个元素
//所以这里 大小是heapSize 已经是-1了 进行下沉
heapify(arr,0,heapSize);
//下沉完堆顶就是次值最大值 与当前堆底交换 大小-1
swap(arr,0,--heapSize);
//次数+1
count++;
}
int[] res = new int[k];
for(int i = 0,j=n-1; i < k; i++,j--){
res[i] = arr[j];
}
return res;
}
//构建大根堆 自底向上
public static void heapify(int[] arr, int i, int n){
int left = 2*i+1; //定义i的左子节点
while(left < n){ //当前下层左节点不越界 还在数组长度内就进行下沉操作
//返回左右节点的较大值 需注意右节点是否会越界的判断
int largest = left + 1 < n && arr[left+1] > arr[left] ? left+1 : left;
//然后较大值与当前i比较 如果i大就刷新最大值
largest = arr[largest] > arr[i] ? largest : i;
if(largest == i){
break; //表示i当前自己最大 那么就不用下沉 直接退出
}
//否则就需要进行交换,并且当前i位置下沉到较大的子节点 该位置的左节点也要刷新来到新的位置
swap(arr,i,largest);
i = largest;
left = i*2+1;
}
}
public static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
// 方法三,时间复杂度O(n + k * logk)
public static int[] maxTopK3(int[] arr, int k) {
//边界判断
if(arr == null || arr.length == 0)
return new int[0];
//判断k 如果超过了数组长度 那么就返回数组长度 避免越界异常
int n = arr.length;
k = Math.min(n,k);
//O(n)
//题目要求是 top k个最大的数 这个第个k最大的数 我们可以转换成 第n-k个最小的数
//通过函数调用 得到这个数 num
int num = minKth(arr,n-k);
//得到该值后 就遍历整个数组arr 把大于num 的数添加到结果数组中
int[] res = new int[k];
int index = 0;
for(int i = 0; i < n; i++){
if(arr[i] > num){
res[index++] = arr[i];
}
}
//添加完之后 可能长度不一定就到k个了 可能会存在与num相等的 并且大于num的还没到k个 也还存在与num相等的 那么就接着补充
//不能在前面大于的循环添加 因为不确定是否存在等于的 一起处理可能会把大于num的数给漏了
for(;index<k;index++ ){
res[index] = num;
}
//O(k * logk)
Arrays.sort(res); //先进行升序排序
//最后我们将数组逆序 变成降序 原数组进行首尾交换的方式
for(int i = 0, j = k-1;i < j;i++,j--){
swap(res,i,j);
}
return res;
}
//时间复杂度O(N) 求数组中 第index小的数
public static int minKth(int[] arr, int index){
//非递归的写法 定义左右边界范围 处于中间区的范围数组 划分值
int l = 0;
int r = arr.length - 1;
int[] range = null;
int pivot = 0;
//左边界小于右边界 正常遍历 直到边界相遇 退出
while( l < r){
//划分值 随机得到 l + [0,R-L]
pivot = arr[l + (int)(Math.random()*(r - l +1))];
//调用函数 荷兰国旗返回 中心区的边界数组
range = partition(arr,l,r,pivot);
if(index < range[0]){
//当前位置小于中间区左边界 说明值就在左侧 r边界往左
r = range[0]-1;
}else if(index > range[1]){
l = range[1]+1;
}else{
//如果当前位置处于中心区 那么就表示当前已经找到 返回该值
return pivot;
}
}
return arr[l]; //遍历完退出的话 l==r只有一个值 该值就是对应的第index小的数
}
public static int[] partition(int[] arr,int l, int r, int pivot){
int less = l-1; //小于区 右边界
int more = r+1; //大于区 左边界
int cur = l; //当前开始遍历位置 第一个
while(cur < more){ //当前位置 往右遍历 直到遇到大于区的左边界就跳出循环
if(arr[cur] < pivot){ //小于划分值 小于区的下个位置 与 当前位置交换 less边界+1 cur也要往后一个
swap(arr, ++less, cur++);
}else if(arr[cur] > pivot){ //大于 那么就是 大于区的前个位置 与当前位置交换 cur此时还不能++ 因为后面交换过来的还没处理 大于区左边界-- 扩大
swap(arr,--more, cur);
}else{
cur++; //相等情况 直接到下个位置
}
}
return new int[]{less+1,more-1};
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
// [-? , +?]
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
public static boolean isEqual(int[] arr1, int[] arr2) {
if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) {
return false;
}
if (arr1 == null && arr2 == null) {
return true;
}
if (arr1.length != arr2.length) {
return false;
}
for (int i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
return false;
}
}
return true;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
// 生成随机数组测试
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
boolean pass = true;
System.out.println("测试开始,没有打印出错信息说明测试通过");
for (int i = 0; i < testTime; i++) {
int k = (int) (Math.random() * maxSize) + 1;
int[] arr = generateRandomArray(maxSize, maxValue);
int[] arr1 = copyArray(arr);
int[] arr2 = copyArray(arr);
int[] arr3 = copyArray(arr);
int[] ans1 = maxTopK1(arr1, k);
int[] ans2 = maxTopK2(arr2, k);
int[] ans3 = maxTopK3(arr3, k);
if (!isEqual(ans1, ans2) || !isEqual(ans1, ans3)) {
pass = false;
System.out.println("出错了!");
printArray(ans1);
printArray(ans2);
printArray(ans3);
break;
}
}
System.out.println("测试结束了,测试了" + testTime + "组,是否所有测试用例都通过?" + (pass ? "是" : "否"));
}
}
四、 蓄水池算法
解决的问题:
假设有一个源源吐出不同球的机器,
只有装下10个球的袋子,每一个吐出的球,要么放入袋子,要么永远扔掉
如何做到机器吐出每一个球之后,所有吐出的球都等概率被放进袋子里
package class29;
/**
* 解决的问题:
*
* 假设有一个源源吐出不同球的机器,
*
* 只有装下10个球的袋子,每一个吐出的球,要么放入袋子,要么永远扔掉
*
* 如何做到机器吐出每一个球之后,所有吐出的球都等概率被放进袋子里
*/
public class ReservoirSampling {
//定义一个袋子类 袋子数组、数组长度、球号数
public static class RandomBox{
private int[] bag; //袋子数组 根据题意存10个
private int N; //数组长度 根据题意存10个
private int count; //入袋的球号 从1,2,3..
public RandomBox(int capacity){ //类初始化
bag = new int[capacity];
N = capacity;
count = 0;
}
//随机函数 等概率返回 1,num球号范围的随机一个数
public int rand(int num){
return (int)(Math.random()*num)+1;
}
//添加一个球号num 一定概率下是否能入袋子数组 需要刷新数组 count个数+1
public void add(int num){
count++; //首先个数先加1
if(count <= N){
//个数在N个以内的 都是直接入袋 10个
bag[count-1] = num;
}else{
if(rand(num) <= N){
//个数超过10了 11,12... 并且在一定概率下 1...num返回小于等于N 我们就认为这个球可以入袋
//同时需要等概率弹出当前袋子中0,N-1下标的10个球其中一个 进行替换
bag[rand(N)-1] = num;
}
}
}
//返回当前的袋子数组
public int[] choices(){
int[] res = new int[N];
for(int i = 0; i < N; i++){
res[i] = bag[i];
}
return res;
}
}
// 请等概率返回1~i中的一个数字
public static int random(int i) {
return (int) (Math.random() * i) + 1;
}
public static void main(String[] args) {
System.out.println("hello");
int test = 10000;
int ballNum = 17;
int[] count = new int[ballNum + 1];
for (int i = 0; i < test; i++) {
int[] bag = new int[10];
int bagi = 0;
for (int num = 1; num <= ballNum; num++) {
if (num <= 10) {
bag[bagi++] = num;
} else { // num > 10
if (random(num) <= 10) { // 一定要把num球入袋子
bagi = (int) (Math.random() * 10);
bag[bagi] = num;
}
}
}
for (int num : bag) {
count[num]++;
}
}
for (int i = 0; i <= ballNum; i++) {
System.out.println(count[i]);
}
System.out.println("hello");
int all = 100;
int choose = 10;
int testTimes = 50000;
int[] counts = new int[all + 1];
for (int i = 0; i < testTimes; i++) {
RandomBox box = new RandomBox(choose);
for (int num = 1; num <= all; num++) {
box.add(num);
}
int[] ans = box.choices();
for (int j = 0; j < ans.length; j++) {
counts[ans[j]]++;
}
}
for (int i = 0; i < counts.length; i++) {
System.out.println(i + " times : " + counts[i]);
}
}
}