在區間 [start, end] 中隨機抽取 count 個不重複整數,要求時間/空間效率最優。
| 算法 | 思路 | 時間複雜度 | 空間複雜度 | 缺點 |
|---|---|---|---|---|
| 1. 暴力隨機 | 直接 random() 放入 Set | O(count²) 最壞無窮 | O(count) | 重複極多,極慢 |
| 2. 存在性檢查 | List + contains() 去重 | O(count²) | O(count) | 隨 count 增大急劇變慢 |
| 3. 元素移除法 | 預建 List,隨機索引後 remove | O(count·n) 因 remove 導致數組位移 | O(n) n=end-start+1 | 大範圍內存爆炸 |
| 4. 狀態轉移法 | 數組模擬鏈表,被選元素指向下一可用 | O(count) 期望 | O(n) | 需全範圍數組 |
| 5. 遞歸 Floyd | 遞歸構建結果集,衝突時取 end | O(count) | O(count) | 遞歸棧溢出風險 |
| 6. 迭代 Floyd ✅ | 單循環實現 Floyd,無遞歸 | O(count) | O(count) | 最優選 |
import java.util.*;
public class FloydSampling {
private static final Random RAND = new Random();
/**
* @param start 區間起始(含)
* @param end 區間結束(含)
* @param count 抽樣數量,必須 ≤ end-start+1
* @return 不重複隨機數列表,順序即抽樣順序
*/
public static List<Integer> sample(int start, int end, int count) {
if (count > end - start + 1) throw new IllegalArgumentException("count exceeds range");
List<Integer> result = new ArrayList<>(count);
for (int i = end - count + 1; i <= end; i++) {
int r = start + RAND.nextInt(i - start + 1); // [start, i]
if (result.contains(r)) {
result.add(i); // 衝突:直接加入當前最大值 i
} else {
result.add(r);
}
}
return result;
}
public static void main(String[] args) {
List<Integer>> sample = sample(1, 100, 10);
System.out.println(sample); // 例:[7, 42, 15, 89, 3, 56, 22, 91, 11, 68]
}
}
Floyd 算法本質是保留已選元素的有序集合,第 k 輪從 [start, end-count+k] 隨機,若衝突則選 end-count+k。數學歸納法可證明每個組合等概率 1/C(n,count)。
若總體 N 未知或極大(如流式數據),使用 水塘抽樣:
public static <T> List<T> reservoirSample(Iterator<T> data, int k) {
List<T> reservoir = new ArrayList<>(k);
int i = 0;
while (data.hasNext()) {
T item = data.next();
if (i < k) reservoir.add(item);
else {
int r = RAND.nextInt(i + 1);
if (r < k) reservoir.set(r, item);
}
i++;
}
return reservoir;
}
count 較小 → 迭代 Floyd(O(count) 時間/空間)。count 小 → 仍用 Floyd,但改用 HashSet 存結果,contains 為 O(1)。