BFPRT

在一大堆数中求其前k大或前k小的问题,简称TOP-K问题。而目前解决TOP-K问题最有效的算法即是BFPRT算法,其又称为中位数的中位数算法,该算法由Blum、Floyd、Pratt、Rivest、Tarjan提出,最坏时间复杂度为O(n)O(n)。

读者要会快速排序相关知识,如果不会请看这里:

https://blog.csdn.net/hebtu666/article/details/81434236排序,大家在里面找快速排序阅读即可。

 

我们以前写过快排的改进求前k大或前k小,但是快排不可避免地存在退化问题,即使我们用了随机数等优化,最差情况不可避免的退化到了O(N^2),而BFPRT就解决了这个问题,主要的思想精华就是怎么选取划分值。

我们知道,经典快排是选第一个数进行划分。而改进快排是随机选取一个数进行划分,从概率上避免了基本有序情况的退化。而BFPRT算法选划分值的规则比较特殊,保证了递归最小的缩减规模也会比较大,而不是每次缩小一个数。

这个划分值如何划分就是重点。

如何让选取的点无论如何都不会太差。

1、将n个元素划分为n/5个组,每组5个元素
2、对每组排序,找到n/5个组中每一组的中位数; 
3、对于找到的所有中位数,调用BFPRT算法求出它们的中位数,作为划分值。

下面说明为什么这样找划分值。

BFPRT_第1张图片

我们先把数每五个分为一组。

同一列为一组。

排序之后,第三行就是各组的中位数。

我们把第三行的数构成一个数列,递归找,找到中位数。

这个黑色框为什么找的很好。

因为他一定比A3、B3大,而A3、B3、C3又在自己的组内比两个数要大。

我们看最差情况:求算其它的数都比c3大,我们也能在25个数中缩小九个数的规模。大约3/10.

我们就做到了最差情况固定递减规模,而不是可能缩小的很少。

下面代码实现:

public class BFPRT {
//前k小
	public static int[] getMinKNumsByBFPRT(int[] arr, int k) {
		if (k < 1 || k > arr.length) {
			return arr;
		}
		int minKth = getMinKthByBFPRT(arr, k);
		int[] res = new int[k];
		int index = 0;
		for (int i = 0; i != arr.length; i++) {
			if (arr[i] < minKth) {
				res[index++] = arr[i];
			}
		}
		for (; index != res.length; index++) {
			res[index] = minKth;
		}
		return res;
	}
//第k小
	public static int getMinKthByBFPRT(int[] arr, int K) {
		int[] copyArr = copyArray(arr);
		return select(copyArr, 0, copyArr.length - 1, K - 1);
	}

	public static int[] copyArray(int[] arr) {
		int[] res = new int[arr.length];
		for (int i = 0; i != res.length; i++) {
			res[i] = arr[i];
		}
		return res;
	}
//给定一个数组和范围,求第i小的数
	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);
		}
	}
//在begin end范围内进行操作
	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);
        //只不过i等于长度一半,用来求中位数
	}
//经典partition过程
	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;
	}
//五个数排序,返回中位数
	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;
	}
//打印
	public static void printArray(int[] arr) {
		for (int i = 0; i != arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();
	}

	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 };
		// sorted : { 1, 1, 1, 1, 2, 2, 2, 3, 3, 5, 5, 5, 6, 6, 6, 7, 9, 9, 9 }
		printArray(getMinKNumsByBFPRT(arr, 10));

	}
}

 

你可能感兴趣的:(数据结构基础)