首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 生成不重複隨機數的 N 種姿勢:從範圍抽樣到洗牌算法

     編輯:PHP 相關知識

「從 1-100 取 10 個不重複隨機數」、「洗牌發牌」、「抽獎系統去重」是高頻需求。本文系統梳理 6 種主流實現,從簡單到高效,從基礎庫函數到底層算法,附帶性能對比與陷阱避雷。

核心需求分析

  • 範圍:整數區間 [min, max]
  • 數量:count,要求 count <= max - min + 1
  • 不重複:結果集元素唯一
  • 隨機性:均勻分布、不可預測

方案一:range + shuffle + array_slice(最推薦、語義清晰)

<?php
function uniqueRandShuffle($min, $max, $count) {
    if ($count > $max - $min + 1) throw new InvalidArgumentException('數量超過範圍');
    $pool = range($min, $max);     // 建立完整池
    shuffle($pool);                // 原地洗牌(Fisher-Yates)
    return array_slice($pool, 0, $count); // 取前 count 個
}

// 使用
print_r(uniqueRandShuffle(1, 100, 10));
// Array ( [0] => 42 [1] => 17 [3] => 89 ... )

// 大範圍優化:避免構建巨大數組
function uniqueRandShuffleOptimized($min, $max, $count) {
    $pool = range($min, $max);
    // 部分洗牌:只打亂前 count 位(Fisher-Yates 變體)
    for ($i = 0; $i < $count; $i++) {
        $j = mt_rand($i, count($pool) - 1);
        [$pool[$i], $pool[$j]] = [$pool[$j], $pool[$i]];
    }
    return array_slice($pool, 0, $count);
}
?>

優點:代碼極簡、語義明確、均勻隨機、PHP 內建 shuffle 經優化。
缺點:range(1, 10000000) 會佔用大量內存(約 80MB),大範圍慎用。

方案二:mt_rand + array_unique + 循環(直觀、但效率低)

<?php
function uniqueRandLoop($min, $max, $count) {
    $result = [];
    while (count($result) < $count) {
        $result[] = mt_rand($min, $max);
        $result = array_unique($result); // 去重
    }
    return array_values($result); // 重置鍵名
}
// 缺點:隨著結果集增大,碰撞概率指數級上升,極慢
?>

方案三:翻翻法去重(array_flip 雙重翻轉,比 array_unique 快 10 倍+)

<?php
function uniqueRandFlip($min, $max, $count) {
    $result = [];
    while (count($result) < $count) {
        $result[] = mt_rand($min, $max);
        // 翻翻法:key<->value 交換兩次,利用鍵名唯一性去重
        $result = array_flip(array_flip($result));
    }
    shuffle($result); // 打亂順序、重置鍵名
    return $result;
}
// 經驗:array_flip 去重比 array_unique 快 10-50 倍
?>

方案四:集合法(PHP 8.1+ array_is_list、array_unique 優化)

<?php
function uniqueRandSet($min, $max, $count) {
    $set = [];
    while (count($set) < $count) {
        $set[mt_rand($min, $max)] = true; // 鍵名天然去重
    }
    return array_keys($set);
}
// 優點:無需去重函數調用,直接利用哈希表特性,最快
?>

方案五:Reservoir Sampling(水塘抽樣,超大範圍/流式數據、無需構建池)

<?php
/**
 * 從 [min, max] 範圍均勻抽取 count 個不重複數
 * 適用:範圍極大(如 1-1億)、內存受限、流式數據
 * 算法:Reservoir Sampling Algorithm R
 */
function uniqueRandReservoir($min, $max, $count) {
    $range = $max - $min + 1;
    if ($count > $range) throw new InvalidArgumentException('數量超過範圍');
    
    $reservoir = [];
    // 前 count 個直接放入
    for ($i = 0; $i < $count; $i++) {
        $reservoir[] = $min + $i;
    }
    
    // 第 i 個元素(i 從 count 開始)以 count/i 概率替換水塘中隨機位置
    for ($i = $count; $i < $range; $i++) {
        $j = mt_rand(0, $i);
        if ($j < $count) {
            $reservoir[$j] = $min + $i;
        }
    }
    shuffle($reservoir);
    return $reservoir;
}

// 測試:從 1 億取 1000 個,瞬間返回,內存極小
$result = uniqueRandReservoir(1, 100000000, 1000);
print_r(array_slice($result, 0, 10));
?>

方案六:隨機字符串/字符集抽樣(驗證碼、Token、短鏈)

<?php
function randomCharset($length, $charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') {
    $bytes = random_bytes($length); // 密碼學安全隨機字節
    $result = '';
    $clen = strlen($charset);
    for ($i = 0; $i < $length; $i++) {
        $result .= $charset[$bytes[$i] % $clen];
    }
    return $result;
}

// 或者簡單版(非加密場景)
function randomStringSimple($length, $charset = '23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ') {
    $result = '';
    $clen = strlen($charset);
    for ($i = 0; $i < $length; $i++) {
        $result .= $charset[mt_rand(0, $clen - 1)];
    }
    return $result;
}

echo randomCharset(32);  // 32 位安全 Token
echo randomStringSimple(6); // 6 位驗證碼(去除易混淆字符 0/O/1/l/I)
?>

性能大比拼(範圍 1-100000,取 1000 個)

<?php
$min = 1; $max = 100000; $count = 1000;

$cases = [
    'shuffle'      => fn() => uniqueRandShuffle($min, $max, $count),
    'shuffle_opt'  => fn() => uniqueRandShuffleOptimized($min, $max, $count),
    'flip'         => fn() => uniqueRandFlip($min, $max, $count),
    'set'          => fn() => uniqueRandSet($min, $max, $count),
    'reservoir'    => fn() => uniqueRandReservoir($min, $max, $count),
];

foreach ($cases as $name => $fn) {
    $start = microtime(true);
    for ($i = 0; $i < 100; $i++) $fn();
    echo "$name: " . (microtime(true) - $start) . "s\n";
}

/*
典型輸出(PHP 8.2):
shuffle:      0.45s  (構建 10萬元素數組開銷大)
shuffle_opt:  0.012s (部分洗牌極快)
flip:         0.008s
set:          0.006s (最快)
reservoir:    0.009s (大範圍王者,不構建池)
*/
?>

選型決策樹

```mermaid graph TD A[需要不重複隨機數] --> B{範圍大小} B -->|小 < 10萬| C[shuffle / set / flip] B -->|大 ≥ 10萬| D{內存敏感?} D -->|是| E[Reservoir Sampling] D -->|否| F[shuffle_opt / set] C --> G{需要密碼學安全?} G -->|是| H[random_bytes + 映射] G -->|否| I[mt_rand 系列] ```

實戰案例:投票系統填補 200 用戶 × 16 選項

<?php
/**
 * 真實業務:25 幅作品,每用戶投 16 票,不重複
 * 200 用戶數據丟失,需批量補全
 */
function generateVotes($workCount = 25, $voteCount = 16, $userCount = 200) {
    $votes = [];
    for ($u = 0; $u < $userCount; $u++) {
        // 方案:集合法(最快)
        $set = [];
        while (count($set) < $voteCount) {
            $set[mt_rand(1, $workCount)] = true;
        }
        $votes[] = array_keys($set);
    }
    return $votes;
}

$votes = generateVotes();
// 入庫:batch insert user_votes (user_id, work_id)
foreach ($votes as $uid => $works) {
    foreach ($works as $wid) {
        // INSERT INTO user_votes (user_id, work_id) VALUES ($uid, $wid)
    }
}
?>

避坑指南

  • mt_rand 非密碼學安全:Token、Session ID、加密隨機數必須用 random_bytes()/random_int()。
  • srand 固定種子導致可預測:PHP 4.2+ 自動播種,**切勿手動 srand()**。
  • 範圍 < 數量拋異常:參數校驗必不可少。
  • 結果未打亂順序:集合法/翻翻法產生的鍵名有序,**務必 shuffle() 或 array_values() 重置**。
  • 浮點數範圍:range() 只支持整數,浮點數需自行映射。

總結

**小範圍首選 set 法(鍵名去重最快)**,**大範圍/受限內存唯一選 Reservoir Sampling**,**密碼學場景用 random_bytes**。理解 Fisher-Yates 洗牌、Reservoir Sampling、哈希去重三大核心算法,靈活組合應對所有不重複隨機需求。封裝為 RandomHelper::uniqueInts() 統一項目口徑,拒絕重複造輪子。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved