一道電面題目, 分為兩問:
- 設計一個系統, 不斷接收數據包(數據內容可以簡單想成一個int值). 給定常量M, 要求從所有獲取的數據中隨機抽樣M個, 每個樣本被抽取的概率相等.
- 如果已接收數據包的數量還未超過M個, 則將它們全部返回.
- 接收數據包的總量是未知的, 可能非常大.
- 機器的存儲空間有限, 無法存儲所有數據包. 但是存儲M個數據包還是綽綽有余的.
- 假如有K臺機器, 如何將第一問的算法做成分布式的, 以最大化吞吐量?
第一問是一個標準的水塘抽樣算法(Reservoir Sampling)問題.
class System {
private:
vector<int> v;
int capacity;
int cnt;
public:
System(int capacity) : capacity(capacity), cnt(0) {}
void put(int n) {
++cnt;
if (cnt <= capacity) {
v.push_back(n);
return;
}
int index = rand() % cnt;
if (index < capacity) {
v[index] = n;
}
}
vector<int> get() {
return v;
}
};
算法思路:
維護一個大小為M
的數組. 記當前接收的是第N
個數據(從1
開始).
- 如果
N<=M
, 直接插入 - 如果
N>M
, 就取一個1~N
之間的隨機數index
. 如果index
在1~M
之間, 則用新接收的數據替換第index
個數據; 否則丟棄.
證明:
假設當前是第M+1
個元素, 它被丟棄的概率是1/(M+1)
, 留下的概率就是M/(M+1)
. 對于已經在集合中的M
個元素, 每個以1/(M+1)
的概率被丟棄, 留下的概率也是M/(M+1)
.
假設當前是第M+2
個元素, 它被丟棄的概率是2/(M+2)
, 留下的概率是M/(M+2)
. 對于前M+1
個元素, 它們在集合中的概率是M/(M+1)
(見上一個分析). 這一次, 它們每個被以1/(M+2)
的概率被丟棄, 留下的概率就是 M/(M+1) * (M+1)/(M+2) = M/(M+2)
依次類推, 到接受第N
個元素時, 每個元素被抽取的概率就是M/N
.
第二問就是分布式的蓄水池抽樣問題了.
算法思路是:
假設有K
個機器, 每個機器維護大小為M
的數組, 并記錄該機器接受的數據總數Ni
.
- 當機器獲取新數據時, 進行單機的蓄水池抽樣.
- 當進行采樣時, 重復
M
次以下操作:
取隨機數d
在[0,1)
之間, 記N=Sum(Ni | i=1...K)
- 若
d<N1/N
則從第一個機器上等概率抽取一個元素. - 若
N1/N<=d<(N1+N2)/N
則從第二個機器上等概率抽取一個元素 - 依此類推.
- 若
假設Ni>M
:
因為第i
個機器上數據的留存概率為M/Ni
, 而采樣時又以Ni/N
的概率抽取該機器, 又以1/M
的概率等概率不放回地選取一個元素, 所以第i
個機器上一個數據被抽中的概率為M/Ni * Ni/N * 1/M = 1/N
. 這樣重復M
次, 每個元素被抽取到的概率就是M/N
.
假設Ni<=M
第i
個機器上數據的留存概率為1
, 采樣時以Ni/N
的概率抽取該機器, 又以1/Ni
的概率等概率不放回地選取一個元素, 所以第i
個機器上一個數據被抽中的概率為1/N
. 同樣, 重復M
次讓每個元素被抽取到的概率為M/N
.