bfprt算法、蓄水池算法

我们先来看一道题  在无序数组中,求第K小的数  这个问题是不是很常见,那么正常的解法就是你把这个数组排序,然后拿出第K小的数,当然,这一种方法大家都会,但是,我们要想一下,如果我们把改数据排序,需要承担一个O(n*logn)的一个时间复杂度,至于把第K小的数拽出来,是O(1)的,那么时间复杂度就是O(n*logn),而该问题能做到最好的时间复杂度是什么呢? O(n) 。我们先介绍一种足够优良的方法,就是改写快排,事实上,这种足够优良的方法,已经是最优解了。你不是要求第K小的数嘛,我们再该无序数组中,随机选个一数,这个数假设叫V,然后呢,我们那这个数V在整个无序数组中做荷兰国旗问题,啥意思,就是小于V的放左边,等于V的放中间,大于V的放右边,在快排中,我们知道,做成这件事,是O(n)的时间复杂度,然后我们就看等于V的部分,有没有把整个数组第K小命中到,啥意思,比如说你要求第100小的数,而你等于V的下标是50-150,那就说明等于V的部分命中第100小嘛,那第K小的数就是V嘛,如果命中了,就停,如果没命中,假如说等于的下标是 500 - 600,那么第K小的数就再V的左边嘛所以我们再左侧再选一个数V'然后重复,知道命中为止,我们总能有找到第K小的时候。

代码:

public static class MaxHeapComparator implements Comparator<Integer> {

		@Override
		public int compare(Integer o1, Integer o2) {
			return o2 - o1;
		}

	}

	// 利用大根堆,时间复杂度O(N*logK)
	public static int minKth1(int[] arr, int k) {
		PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new MaxHeapComparator());
		for (int i = 0; i < k; i++) {
			maxHeap.add(arr[i]);
		}
		for (int i = k; i < arr.length; i++) {
			if (arr[i] < maxHeap.peek()) {
				maxHeap.poll();
				maxHeap.add(arr[i]);
			}
		}
		return maxHeap.peek();
	}

	// 改写快排,时间复杂度O(N)
	// k >= 1
	public static int minKth2(int[] array, int k) {
		int[] arr = copyArray(array);
		return process2(arr, 0, arr.length - 1, k - 1);
	}

	public static int[] copyArray(int[] arr) {
		int[] ans = new int[arr.length];
		for (int i = 0; i != ans.length; i++) {
			ans[i] = arr[i];
		}
		return ans;
	}

	// 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) {
		if (L == R) { // L = =R ==INDEX
			return arr[L];
		}
		// 不止一个数  L +  [0, R -L]
		int pivot = arr[L + (int) (Math.random() * (R - L + 1))];
		int[] range = partition(arr, L, R, pivot);
		if (index >= range[0] && index <= range[1]) {
			return arr[index];
		} else if (index < range[0]) {
			return process2(arr, L, 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) {
		int less = L - 1;
		int more = R + 1;
		int cur = L;
		while (cur < more) {
			if (arr[cur] < pivot) {
				swap(arr, ++less, cur++);
			} else if (arr[cur] > pivot) {
				swap(arr, cur, --more);
			} else {
				cur++;
			}
		}
		return new int[] { less + 1, more - 1 };
	}

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

我们通过改写快排的方式求出了这个问题的最优解,注意,改写快排的方式求出的是一个时间复杂度的期望,因为如果非要较真的话,划分值V每次都选出最差的,其实时间复杂度是n的平方,那有的同学就要问了,求时间复杂度不是按最差的算嘛,为啥这里不按最差的算,原因是我们的划分值是随机选的,是概率事件,我们的划分值V不可能每次都是最差的情况,通过我们再概率论中的数学期望,求得该方法是收敛于O(n)的,那么又有小伙伴要说了,那以后会不会出现一个比O(n)更小的时间复杂度啊, 不可能,一个无序数组,要找第K小的数,我们看完一遍数都O(n)了,怎么可能在没有看完数组的情况下就知道第K小的是什么呢。

bfprt介绍

       这个算法怎么这么怪,连原因都没有,是,他是五个大牛发明的一个算法,每个大牛的首字母拎出来,组成的一个算法,他是解决什么问题的,还是在无序数组中找到第K小的数,你不是说有最优解了嘛,为什么还需要bfprt,因为上面这种方法确实是最优解,时间复杂度也很棒,空间复杂度也是极致,但是他是用概率来解的,bfprt它不是用概率来解释的。

  其实我们上面介绍的算法已经很优秀了,无论是笔试还是我们平时工作中需要用到该算法,我们拿上面的写法写就已经很可以了,它就是最优解,而bfprt就是在面试中用来给面试官聊,让它知道你知道这么一个经典的算法。因为bfprt算法,上了算法导论,他就是第九章的内容,而且第九章还有关于bfprt严格的证明,如此重要的一个经典的算法,用来装逼再适合不过了。

bfprt算法

      上面这个算法,是随机选一个数,小于的放左边,大于的放右边,随机选这件事,我们可以认为最差和最好都是概率事件,既然是概率事件,那么不可能一直差,也不可能一直好,长期收敛于O(n),bfprt算法啊,它所有的核心都是聚焦于怎么选这个P上,上面的算法,很简单,随机选一个,但是我们的bfprt算法,选P一个巨讲究的事,bfprt算法和上面的算法,唯一的区别就在于P的选取。下面我们来看这个P怎么选取

1) 这个数组, 每五个数分一组,如果最后一组不够五个,有多少算多少。

2)这五个数在小范围上排个序,不要求整体有序,要求小组内部有序,整个时间复杂度O(n)

3)每个小组中的中位数拿出来,最后一组如果是偶数,拿上中位数。

4) 求每个小组中位数组成的数组的中位数

那这个中位数怎么求呢,我们先看bfprt是这么调的  bfprt(arr,k),代表在arr这个数组中,求第k小的数,每个小组中位数组成的数组,我们称之为 m  ,数组长度为 N/5  ,我们这么调 bfprt(m,N/10)   整个长度是N/5  那么 N/10  不就是第中间小的嘛,不就是中位数嘛。

那你说bfprt为啥扯这么多去选划分值啊,扯这么多肯定是有好处的啊,什么好处,好处就是小于P或者大于P有一个至少的规模。

为什么要五个数一组?

3个数一组也能收敛于O(n),7个数一组也能收敛于O(n),那为啥是五个数一组,因为是五个人发明的,就这么简单,人家跟5有缘分

bfprt算法其实在算法上的地位是很高的,为啥呢,就是它提出了一种东西,就是我尽量选一个平庸,或者平凡的一个分界,去优化我整个行为,比如说我随机选一个,那么算计选这件事可能是频繁的,可能是很差的,但是在数学上证明呢能够收敛于O(n),进而我就要想,我不想随机选的话,我就要选出一种能够确定的淘汰一定比例的特殊的划分值,进而能够规避掉最差的情况,使得我的算法能够拥有严格的时间复杂度,这是bfprt算法带给整个算法发展最重要的思想。所以它的江湖地位还是蛮高的。

接下来看代码:

public class FindMinKth {

	public static class MaxHeapComparator implements Comparator<Integer> {

		@Override
		public int compare(Integer o1, Integer o2) {
			return o2 - o1;
		}

	}

	// 利用大根堆,时间复杂度O(N*logK)
	public static int minKth1(int[] arr, int k) {
		PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new MaxHeapComparator());
		for (int i = 0; i < k; i++) {
			maxHeap.add(arr[i]);
		}
		for (int i = k; i < arr.length; i++) {
			if (arr[i] < maxHeap.peek()) {
				maxHeap.poll();
				maxHeap.add(arr[i]);
			}
		}
		return maxHeap.peek();
	}

	// 改写快排,时间复杂度O(N)
	// k >= 1
	public static int minKth2(int[] array, int k) {
		int[] arr = copyArray(array);
		return process2(arr, 0, arr.length - 1, k - 1);
	}

	public static int[] copyArray(int[] arr) {
		int[] ans = new int[arr.length];
		for (int i = 0; i != ans.length; i++) {
			ans[i] = arr[i];
		}
		return ans;
	}

	// 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) {
		if (L == R) { // L = =R ==INDEX
			return arr[L];
		}
		// 不止一个数  L +  [0, R -L]
		int pivot = arr[L + (int) (Math.random() * (R - L + 1))];
		int[] range = partition(arr, L, R, pivot);
		if (index >= range[0] && index <= range[1]) {
			return arr[index];
		} else if (index < range[0]) {
			return process2(arr, L, 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) {
		int less = L - 1;
		int more = R + 1;
		int cur = L;
		while (cur < more) {
			if (arr[cur] < pivot) {
				swap(arr, ++less, cur++);
			} else if (arr[cur] > pivot) {
				swap(arr, cur, --more);
			} else {
				cur++;
			}
		}
		return new int[] { less + 1, more - 1 };
	}

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

	// 利用bfprt算法,时间复杂度O(N)
	public static int minKth3(int[] array, int k) {
		int[] arr = copyArray(array);
		return bfprt(arr, 0, arr.length - 1, k - 1);
	}

	// arr[L..R]  如果排序的话,位于index位置的数,是什么,返回
	public static int bfprt(int[] arr, int L, int R, int index) {
		if (L == R) {
			return arr[L];
		}
		// L...R  每五个数一组
		// 每一个小组内部排好序
		// 小组的中位数组成新数组
		// 这个新数组的中位数返回
		int pivot = medianOfMedians(arr, L, R);
		int[] range = partition(arr, L, R, pivot);
		if (index >= range[0] && index <= range[1]) {
			return arr[index];
		} else if (index < range[0]) {
			return bfprt(arr, L, range[0] - 1, index);
		} else {
			return bfprt(arr, range[1] + 1, R, index);
		}
	}

	// arr[L...R]  五个数一组
	// 每个小组内部排序
	// 每个小组中位数领出来,组成marr
	// marr中的中位数,返回
	public static int medianOfMedians(int[] arr, int L, int R) {
		int size = R - L + 1;
		int offset = size % 5 == 0 ? 0 : 1;
		int[] mArr = new int[size / 5 + offset];
		for (int team = 0; team < mArr.length; team++) {
			int teamFirst = L + team * 5;
			// L ... L + 4
			// L +5 ... L +9
			// L +10....L+14
			mArr[team] = getMedian(arr, teamFirst, Math.min(R, teamFirst + 4));
		}
		// marr中,找到中位数
		// marr(0, marr.len - 1,  mArr.length / 2 )
		return bfprt(mArr, 0, mArr.length - 1, mArr.length / 2);
	}

	public static int getMedian(int[] arr, int L, int R) {
		insertionSort(arr, L, R);
		return arr[(L + R) / 2];
	}

	public static void insertionSort(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,返回topK个最大的数不同时间复杂度的三个方法:

1)O(N*logN)

2) O(N+K*logN)

3) O(n+K*logK)

第一种,没啥好说的,都O(N*logN)了,我们先排序,在直接取K个就行了

第二种,我们可以使用加强堆,对于已经有的数组,我们有一种方式是可以通过heapInsert将一个数组调整成大根堆,然后我们把最大的数弹出,然后调整堆,使其成为大根堆,然后再弹出,弹出K个。

第三种就用到我们刚刚介绍的啦,我们先找到第 N-K小的数字,然后我们再遍历一遍数组,比该数字大的都收集起来,就求出来啦

public class MaxTopK {

	// 时间复杂度O(N*logN)
	// 排序+收集
	public static int[] maxTopK1(int[] arr, int k) {
		if (arr == null || arr.length == 0) {
			return new int[0];
		}
		int N = arr.length;
		k = Math.min(N, k);
		Arrays.sort(arr);
		int[] ans = new int[k];
		for (int i = N - 1, j = 0; j < k; i--, j++) {
			ans[j] = arr[i];
		}
		return ans;
	}

	// 方法二,时间复杂度O(N + K*logN)
	// 解释:堆
	public static int[] maxTopK2(int[] arr, int k) {
		if (arr == null || arr.length == 0) {
			return new int[0];
		}
		int N = arr.length;
		k = Math.min(N, k);
		// 从底向上建堆,时间复杂度O(N)
		for (int i = N - 1; i >= 0; i--) {
			heapify(arr, i, N);
		}
		// 只把前K个数放在arr末尾,然后收集,O(K*logN)
		int heapSize = N;
		swap(arr, 0, --heapSize);
		int count = 1;
		while (heapSize > 0 && count < k) {
			heapify(arr, 0, heapSize);
			swap(arr, 0, --heapSize);
			count++;
		}
		int[] ans = new int[k];
		for (int i = N - 1, j = 0; j < k; i--, j++) {
			ans[j] = arr[i];
		}
		return ans;
	}

	public static void heapInsert(int[] arr, int index) {
		while (arr[index] > arr[(index - 1) / 2]) {
			swap(arr, index, (index - 1) / 2);
			index = (index - 1) / 2;
		}
	}

	public static void heapify(int[] arr, int index, int heapSize) {
		int left = index * 2 + 1;
		while (left < heapSize) {
			int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
			largest = arr[largest] > arr[index] ? largest : index;
			if (largest == index) {
				break;
			}
			swap(arr, largest, index);
			index = largest;
			left = index * 2 + 1;
		}
	}

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

	// 方法三,时间复杂度O(n + k * logk)
	public static int[] maxTopK3(int[] arr, int k) {
		if (arr == null || arr.length == 0) {
			return new int[0];
		}
		int N = arr.length;
		k = Math.min(N, k);
		// O(N)
		int num = minKth(arr, N - k);
		int[] ans = new int[k];
		int index = 0;
		for (int i = 0; i < N; i++) {
			if (arr[i] > num) {
				ans[index++] = arr[i];
			}
		}
		for (; index < k; index++) {
			ans[index] = num;
		}
		// O(k*logk)
		Arrays.sort(ans);
		for (int L = 0, R = k - 1; L < R; L++, R--) {
			swap(ans, L, R);
		}
		return ans;
	}

	// 时间复杂度O(N)
	public static int minKth(int[] arr, int index) {
		int L = 0;
		int R = arr.length - 1;
		int pivot = 0;
		int[] range = null;
		while (L < R) {
			pivot = arr[L + (int) (Math.random() * (R - L + 1))];
			range = partition(arr, L, R, pivot);
			if (index < range[0]) {
				R = range[0] - 1;
			} else if (index > range[1]) {
				L = range[1] + 1;
			} else {
				return pivot;
			}
		}
		return arr[L];
	}

	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) {
				swap(arr, ++less, cur++);
			} else if (arr[cur] > pivot) {
				swap(arr, cur, --more);
			} 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个球的袋子,最多只有10个球的容量,我想干成一件什么事呢,管子吐出一个具体的X球后呢,你只有两种选择,第一种选择,就是把X球给扔掉,一但你扔掉了,这个球就永远不能找回,或者用一种机制,把球放入袋子中,当然已经放入袋子中的也可以选一个把它扔掉,向达成的效果是,比如你现在吐出的球是第1741个球,你需要做到从第1号球到第1741号球,每一个球进袋子的概率相等。如果吐到第2372号球,我们需要做到从1号球到2372号球,每一个球进到袋子里的概率相等,总而言之,就是不管我们吐到多少号球,从1号球到该号球,每一个球进袋子里的概率相等。他不是说固定多少个来解决这个问题,它是动态的。

怎么做?

在吐出1号球到10号球之间,直接进袋子,概率都是100%,核心在于10号球之后咋办。加入说现在吐出的是 i 号球,我们以 i 分之 10 的概率决定要不要进袋子,如何以 i 分之10 的概率决定进不进袋子,这个函数传如 i 的话,等概率返回 1 到 i 的其中一个数字,如果返回的数字是 1到 10,那么就做这件事,如果返回的数字是大于 10 的就不做这件事,如果它中了,它就一定会进袋子,如果没中,就丢掉,永远不找回, 然后,既然 i 进了,那么袋子里就要有球要出,哪个出呢,等概率随机扔一个 ,流程就结束了

why?

假设我们已经到 1729号球了,那么在1729号球结束后  3号球还在袋子中的概率,  10 号球之前 3号球存活的概率 1  ,11号球来了,这个时候3号球肯定在袋子里,我们现在算 3号球淘汰的概率,如我们的流程, 11 号球来之前,会以 11 分之 10 的概率计算 要不要进袋子,它如果不进袋子,那么3号球就不会被淘汰,它只有决定 11 号球进袋子,他才可能被淘汰,他决定进袋子,那么怎么淘汰 3 号球呢,同时 3 号球非常倒霉,以 1/10 的概率被命中,3号球才会被 11 号球淘汰,所以 10/11 * 1/10 = 1/11  就是 3 号球淘汰的概率。那么它存活的概率就是 10/11 ,这时, 12号球来了,同理 11 号球 ,3号球存活的概率是  11/12 , 13号球 之后 3号球还存活的概率是 12/13  ....  看到规律了嘛,最后 到 1729 号球之后, 3号球还存活的概率为 10/1729  。

3号球很特殊,那我们来求 17 号球活到1729的概率 ,怎么活下来,17号球,在他出现的时候得被选中对吧,概率 10/17 ,18号球来之后 同样 17 号存活的概率 17/18  最后 一乘,10/1729

// 请等概率返回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]);
		}
}

实际应用场景

抽奖系统定时开奖

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值