随机问题
问题描述
对于给定的数组 $Q$ ,将其每个元素随机分布在数组中,并且使得之后每个元素被取到的概率相等?
Shuffle 算法
Code
public static void shuffle(int[] q) {
for (int i = q.length - 1; i >= 0; i--)
swap(q, i, (int) (Math.random() * (i + 1)));
}
随机性证明
设整个数组长度为 $length$,当前元素下标为 $i\in[0,length-1]$ ,每次从 $[0,i]$ 中选择一个元素进行交换。
在第一次交换中,各个元素被选中的概率为 $\frac{1}{i+1}$
第二次交换中,任一元素被选中的概率为 $\frac{i+1}*\frac{1}=\frac{1}{i+1}$
以此类推,各个元素被选中的概率都为 $\frac{1}{i+1}$ ,可得出概率相等
问题描述
那么现在问题升级了,由于数据集比较大并且在不断增加,我们无法将所有数据全部都放到内存中
那么是否有办法在这个数据流(可设其总数为 $I$ )中选出一个元素,使得在时间复杂度 $O(N)$ 的情况下,该元素被选中的概率为与其他元素被选中的概率相等?
蓄水池抽样算法
Code
public class RandomSelect {
public static int sampling(int[] q) {
int t = 0;
for (int i = 1; i <= q.length; i++) {
if (Math.random() < 1 / i) t = q[i - 1];
}
return t;
}
}
随机性证明
设整个数组元素个数为 $length$,当前元素下标为 $i\in[1,length]$ 。
-
当 $i=1$ 时,必然选择第一个元素。
-
当 $i=2$ 时,选择第一个元素的条件为上次筛选被选中,且本次筛选没有被排除,则有 $1*\frac{1}{2}=\frac{1}{2}$ ,
-
当 $i=3$ 时,任然选择第一个元素的概率为 $\frac{1}{2}(1-\frac{1}{3})=\frac{1}{2}\frac{2}{3}=\frac{1}{3}$ ,
依此类推,可得选择任一元素(设其下标为 $j$ )的概率为 $\frac{1}(1-\frac{1}{j+1})...(1-\frac{1})(1-\frac{1})=\frac{1}$ 故该算法可以确保选择出的元素的随机性。
升级
数组抽样问题
问题描述
那么如果要选择出 $K$ 个元素,使得选出的元素在整个数据集(可设其总数为 $I$ )中出现的概率相等
Code
public class RandomSelect {
private static final int N = 5;
private static final int[] q = new int[N];
private static int ptr = 0;
public static int[] sampling(int[] txt, int k) {
for (int i = 1; i <= txt.length; i++) {
if (i <= k) q[idx++] = txt[i - 1];
else {
int j = (int) (Math.random() * i) + 1;
if (j < k) q[j - 1] = txt[i - 1];
}
}
return q;
}
}
随机性证明
设整个数组非空长度为 $N$,当前元素下标为 $i\in[1,length]$ ,要选择的元素个数为 $K$ ,随机数的值为 $R$
-
当 $i\le K$ 时,每个元素被选中进入数组的概率为 $1$ 。
-
当 $i>K$ 时
- 当 $i=K+1$ 时,第 $i$ 个元素被选中进入数组的前提为随机数的值 $R\in[0,K]$ ,则概率为 $\frac{K+1}*\frac{1}=\frac{1}{K+1}$ ,不进入数组的概率则为 $1-\frac{1}{K+1}=\frac{K+1}$
- 由此可得,当 $i=N$ 时,第 $i$ 个元素进入数组的概率为 $\frac*\frac{1}=\frac{1}$,同时不进入数组的概率为 $\frac$
故数组中的每个元素数据不被替换的概率为 $\frac{K+1}\frac{K+1}{K+2}...*\frac=\frac$
分布式抽样问题
问题描述
当数据量规模十分巨大时,单机的内存访问,磁盘 I/O,CPU 频率就会成为数据吞吐量的瓶颈,此时常常采用分布式的结构来分担数据压力,提高吞吐量。
那么如果使用分布式的方式,又要如何保证均匀的取样呢?
算法过程
设分布式架构下有 $N$ 台机器,处理的总数据量为 $S$,取样值为 $M$
- 将整个数据流分为 $N$ 个子流,分布到所有机器上,处理的数据量记为 $N_{1},N_{2},N_{3},...,N_$(同时记 $N_0=0$),同时每台机器都运行一个蓄水池抽样模型,取得 $M$ 个数据
- 生成一个随机数 $R\in[1, S]$,如果 $R\in[\sum{N_0+N_1+...+N_},\sum{N_0+N_1+...+N_}+N_i]$,则从 $N_i$ 的取样中再次取样加入到最终取样中
- 循环第 2 步 $M$ 次
随机性证明
- 在第 $K$ 台机器上取样值的概率为 $\frac$
- 随机数取值在第 $K$ 个区间的概率为 $\frac$
- 在第 $K$ 个取样值中随机取样取到任意元素的概率为 $\frac{1}$,循环取值 $M$ 次
故最后的取样值 $M$ 相对于总数据流数量 $S$ 的概率为 $\frac\frac\frac{1}*M=\frac$,故可以确保随机取样。
RandN 生成 RandM
问题描述
现在提供一个函数 $RandN$ ,它可以提供 $1\sim N$ 的随机数生成,那么如何使用其生成函数 $RandM$,定义同上
Code
// API randN() is already defined.
public class Solution {
// If N is greater than M.
public int randM() {
int res = randN();
while (res > N) res = randM();
return res;
}
// If N is less than M, and N is odd.
public void randM() {
int a = randN(), b = randN();
while (a == N) a = randN();
while (b > (M - N + 1)) b = randN();
return ((a & 1) == 0 ? 0 : N - 1) + b;
}
// If N is less than M, and M is less than N^2.
public void randM() {
int res = randN() * N + randN() - (N + 1);
while (res > (N * N) / M * M) res = randN() * N + randN() - (N + 1);
// goto 6:
return res % M + 1;
}
}
随机性证明
-
当 $M$ 小于 $N$ 时
- 已知 $randN$ 可以生成 $1\sim N$,如果舍弃大于 M 的部分,则对于 $1\sim M$ 来说必然也是等概率的
-
当 $M$ 大于 $N$ 时
-
若 $N$ 为奇数,则取 $a = 1\sim (N - 1)$ 的部分,数量和为偶数,故 $a$ 获得奇数偶数的概率相等,则将奇数与偶数之一映射为 $0 \space or\space (N - 1)$。(其实映射成多少都没问题,只要等概率,同时和为 $M$)
取 $b = 1\sim (M - (N - 1))=1\sim(M - N + 1)$,此时要求 $M - N + 1 \le N$,则 $b+a=(0\space or\space (N - 1))+(1\sim (M-N+1))=1\sim M$
-
通法。
生成 $0\sim N2-1$ 之间的随机数,再舍去大于 $\frac{N{2}}*M$ 的部分并取余,即可将 $0\sim \frac{N^{2}}*M$ 映射为 $0\sim M - 1$,再对其进行 $+1$ 操作即可
-
Top K 问题
问题描述
在一个数据集中,选择出第 $K$ 大的元素
快速排序
可以将所有数据读取到内存中排序并取下标取值
Code
public class TopK {
public static int get(int[] q, int k) {
sort(q, 0, q.length - 1);
retunr q[k - 1];
}
private static void sort(int[] q, int l, int r) {
if (l >= r) return;
int i = l - 1, j = r + 1, k = q[l + r >> 1];
while (i < j) {
do i++; while (q[i] < k);
do j--; while (q[j] > k);
if (i < j) swap(q, i, j);
}
sort(q, l, j); sort(q, j + 1, r);
}
}
时间复杂度
由于每一次调用函数都遍历了整个数组,同时每一次递归调用都会将数组二分,直到区间中只有一个元素
故时间复杂度为 $O(N*logN)$ 即 $O(NlogN)$
堆排序
将所有数据依次读取到堆中,同时维持堆大小为 $K$
Code
public class TopK {
private static final PriorityQueue<Integer> que = new PriorityQueue<>();
public static void get(int[] q, int k) {
for (var i : q) {
if (que.size() < k) que.add(i);
else if (que.peek() < i) {
que.poll();
que.add(i);
}
}
return que.peek();
}
}
时间复杂度分析
首先需要遍历整个数据集,同时堆进行大小为 $K$ 排序(底层为树的排序,故时间复杂度 $logK$),故时间复杂度为 $O(NlogK)$
快速选择算法
快排思想 + 二分思想,每次选取一半的区间(注意:在使用时需要将 t 的值赋 成 t - 1)
Code
public class TopK {
public static int quickSelect(int[] q, int l, int r, int t) {
if (l == r) return q[k];
int i = l - 1, j = r + 1, k = q[l + r >> 1];
while (i < r) {
do i++; while (q[i] < k);
do j--; while (q[j] > k);
if (i < j) swap(q, i, j);
}
if (k <= j) return quickSelect(q, l, j, k);
else return quickSelect(q, j + 1, r, k);
}
}
时间复杂度分析
第一次遍历整个数组 $N$ ,第二次遍历选择出的数组的一半,期望值为 $O(\frac{2})$ ···
由极限收敛可得,时间复杂度为 $O(\sum{N +\frac{2}+\frac{4}+...})=O(N *\sum{1+\frac{1}{2}+\frac{1}{4}+...})=O(2N)=O(N)$