業務常需生成「8 位不重複數字」作為訂單號、邀請碼、驗證碼。本文分析三種演算法的時空複雜度、碰撞概率與適用場景,附生產級代碼實現。
0-912345678 合法,11234567 非法)核心思想: 從備選池 [0-9] 隨機抽取,抽取後與末尾元素交換並縮小池範圍。時間複雜度 O(k),k=8,空間 O(10)。
public class RandomNumberGenerator {
private static final int LENGTH = 8;
private static final int POOL_SIZE = 10; // 0-9
private static final Random RANDOM = new Random(); // 線程安全但有爭用,高併發建議 ThreadLocalRandom
/**
* 費雪-耶茨洗牌變體:部分洗牌
* @return 8 位不重複數字字符串
*/
public static String generateNumber() {
int[] pool = new int[POOL_SIZE];
for (int i = 0; i < POOL_SIZE; i++) pool[i] = i; // 初始化 0-9
int[] result = new int[LENGTH];
int available = POOL_SIZE;
for (int i = 0; i < LENGTH; i++) {
int idx = RANDOM.nextInt(available); // [0, available)
result[i] = pool[idx];
// 將已用元素換到末尾,縮小可用範圍
swap(pool, idx, available - 1);
available--;
}
return arrayToString(result);
}
private static void swap(int[] arr, int i, int j) {
int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp;
}
private static String arrayToString(int[] arr) {
StringBuilder sb = new StringBuilder(arr.length);
for (int v : arr) sb.append(v);
return sb.toString();
}
}
優勢: 理論保證不重複、無重試循環、效能極高、邊界情況 (LENGTH=10) 自然支援全排列。
public static String generateWithSet() {
Set set = new LinkedHashSet<>(); // 保持插入順序
Random random = new Random();
while (set.size() < LENGTH) {
set.add(random.nextInt(10)); // 0-9
}
StringBuilder sb = new StringBuilder();
for (Integer v : set) sb.append(v);
return sb.toString();
}
缺點: 隨著集合填滿,碰撞概率指數上升 (最後一位命中率 1/10),高併發下耗時不確定;LinkedHashSet 額外記憶體開銷。
當數字池擴大到 0-9999 (生成 4 位不重複) 或更大時,布隆過濾器或 BitSet 可大幅降低記憶體:
// 適用於大池 (如 0-999999 生成 6 位不重複)
public static String generateLargePool(int poolSize, int length) {
BitSet used = new BitSet(poolSize);
Random random = new Random();
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int num;
do { num = random.nextInt(poolSize); } while (used.get(num));
used.set(num);
sb.append(String.format("%06d", num)); // 定寬補零
}
return sb.toString();
}
| 演算法 | 平均耗時 (ms) | 記憶體 | 碰撞重試 | 線程安全 |
|---|---|---|---|---|
| Fisher-Yates (陣列交換) | ~120 | 極低 (40 bytes) | 零 | 需 ThreadLocalRandom |
| HashSet 去重 | ~480 | 中等 | 高 (末期) | 需同步或 ThreadLocal |
| BitSet 大池 | ~210 | 低 (poolSize/8 bytes) | 低 | 需同步 |
import java.util.concurrent.ThreadLocalRandom;
public class SecureIdGenerator {
private static final int LENGTH = 8;
/**
* 高併發安全、效能最優
*/
public static String generate() {
int[] pool = {0,1,2,3,4,5,6,7,8,9};
int[] result = new int[LENGTH];
int available = 10;
ThreadLocalRandom random = ThreadLocalRandom.current();
for (int i = 0; i < LENGTH; i++) {
int idx = random.nextInt(available);
result[i] = pool[idx];
pool[idx] = pool[--available]; // 交換並縮小範圍 (單行優化)
}
// 直接格式化,避免 StringBuilder 開銷
return String.format("%08d", arrayToInt(result)); // 注意:arrayToInt 需處理前導零
}
// 更簡潔:直接構建字符串
public static String generateFast() {
char[] chars = "0123456789".toCharArray();
char[] result = new char[LENGTH];
int available = 10;
ThreadLocalRandom random = ThreadLocalRandom.current();
for (int i = 0; i < LENGTH; i++) {
int idx = random.nextInt(available);
result[i] = chars[idx];
chars[idx] = chars[--available];
}
return new String(result);
}
}
new Random().nextInt(10) 在循環中創建實例 → 種子相同導致序列重複,**務必複用 Random 實例**Math.random() → 內部同步鎖爭用,高併發效能差1234567 只有 7 位,**必須補零格式化**若需「字母數字混合 10 位不重複」,擴大字符池至 36 (0-9, A-Z) 即可,演算法不變。分佈式環境下「全局不重複」需引入 Snowflake、UUID 或資料庫號段模式,單機不重複演算法不再適用。
選對演算法,能讓簡單的 ID 生成從「業務痛點」變成「技術亮點」。