我们先来看一道题 在无序数组中,求第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]);
}
}
实际应用场景
抽奖系统定时开奖