rand() + % 无法保证不重复,因其值域有限且%放大偏差;正确做法是用std::shuffle打乱全集后截取,或小样本时用std::uniform_int_distribution配合std::unordered_set拒绝采样。

为什么 rand() + % 生成不重复随机数大概率失败
因为 rand() 值域有限(通常为 0–32767),且 % 操作会放大低比特偏差;更关键的是,它本身不提供“不重复”保障——你得自己去重或打乱,硬靠 while 循环检查已存在值,在范围大、数量多时容易卡住甚至死循环。
- 典型错误现象:
std::set插入反复失败,或vector查重耗时飙升(O(n²)) - 真实场景:抽 100 个不重复的 ID(范围 1–10000),不是掷骰子模拟
- 根本问题:
rand()是伪随机序列,但“不重复”是集合操作,不是随机生成逻辑能直接覆盖的
用 std::shuffle 打乱容器是最稳的方案
不重复的本质是“从全集里等概率取样”,最可靠做法是先构造完整候选集,再用 Fisher-Yates 打乱,最后截取前 N 个——std::shuffle 就是标准库对它的实现,线性时间、无偏差、不依赖 rand()。
- 必须配合
std::random_device和std::mt19937:用std::shuffle(v.begin(), v.end(), gen),其中gen是std::mt19937{std::random_device{}()} - 别用
std::random_shuffle(C++17 已弃用,且默认用rand()) - 内存权衡:若全集太大(如 1e9 范围只取 10 个),不能建完整 vector,得换算法(见下一条)
std::vector<int> candidates(1000);
std::iota(candidates.begin(), candidates.end(), 1); // 1~1000
std::shuffle(candidates.begin(), candidates.end(), std::mt19937{std::random_device{}()});
std::vector<int> result(candidates.begin(), candidates.begin() + 100);
小样本大范围?用 std::unordered_set + std::uniform_int_distribution
当全集远大于所需数量(比如从 1–1000000 里选 10 个),建完整数组浪费内存,此时用拒绝采样更实际——但必须控制好上限,避免无限重试。
- 关键点:用
std::uniform_int_distribution替代%,保证均匀;用std::unordered_set查重 O(1) 平均 - 安全底线:加重试计数,超过
expected_attempts * 5就报错(比如期望 10 次却试了 50 次,说明密度太低或分布异常) - 别用
std::set——红黑树插入 O(log n),没必要;也别用vector::find——O(n) 查重会退化
std::unordered_set<int> seen;
std::mt19937 gen{std::random_device{}()};
std::uniform_int_distribution<int> dist(1, 1000000);
while (seen.size() < 10) {
int x = dist(gen);
seen.insert(x); // insert 自动去重
}
跨平台或嵌入式环境要注意 std::random_device 可能不可用
某些编译器/平台(如 MinGW、部分 iOS 工具链)下 std::random_device 实际退化为固定种子的 mt19937,导致每次运行序列相同——这不是 bug,是规范允许的“尽力而为”行为。
立即学习“C++免费学习笔记(深入)”;
- 验证方法:连续两次
std::random_device{}()是否相等;若恒定,就得手动喂熵(比如用time(nullptr) ^ getpid(),仅限非安全场景) - 生产环境别依赖它生成密钥或防作弊逻辑;做测试或游戏抽卡够用,但得心里有数
- 如果连
std::mt19937都不用(比如裸机),就老实用 Knuth 的简化版 Fisher-Yates + 硬件 timer 计数器当 seed
生成不重复随机数真正的复杂点不在“怎么写循环”,而在判断“要不要建全集”——范围和数量的比值决定算法分水岭,这个阈值没人帮你算,得自己按内存和速度权衡。











