最可靠方案是用 std::shuffle 配合 std::mt19937 引擎打乱已填充的容器,本质为 Fisher-Yates 算法,天然不重复且均匀;禁用 rand() 和手写查重循环,容器须支持随机访问迭代器。

用 std::shuffle 生成不重复随机数最可靠
直接结论:不要手写“while 循环 + set 查重”,也不要用 rand() 配模运算——既低效又易偏态。C++11 起,std::shuffle 是标准、高效、无偏的首选方案,本质是 Fisher-Yates 洗牌算法,天然保证不重复。
它不生成“随机数”,而是打乱一个已知不重复的序列(比如 0,1,2,...,n-1),所以结果必然不重复,且均匀分布。
- 必须传入真随机数引擎,如
std::mt19937,不能用默认构造的std::default_random_engine(种子固定,每次运行结果一样) - 容器必须支持随机访问迭代器,
std::vector、std::array可用,std::list不行 - 打乱前需先填充目标范围,例如要 10 个不重复的 1~100 之间的数,就先构造含 1~100 的 vector,再 shuffle 后取前 10 个
std::shuffle 的典型用法和常见错误
正确写法依赖三个要素:容器、随机引擎、适配器。漏掉任意一个都会出问题。
std::vectornums; for (int i = 1; i <= 100; ++i) nums.push_back(i); std::random_device rd; std::mt19937 g(rd()); // 必须用 rd() 初始化,否则全是同一序列 std::shuffle(nums.begin(), nums.end(), g); // 注意:是 [begin, end),不是 end-1
- 错误:用
std::rand当第三个参数 → 编译失败,std::shuffle要求引擎满足 UniformRandomBitGenerator 概念 - 错误:写成
std::shuffle(v.begin(), v.end()-1, g)→ 最后一个元素永远不动 - 错误:在循环里反复创建
std::mt19937并用相同种子 → 所有 shuffle 结果一模一样
如果只要几个不重复随机数,别 shuffle 整个大数组
比如从 1~10000 中抽 5 个不重复数,没必要构造一万个 int 再 shuffle。这时改用 std::sample(C++17)更省空间和时间:
立即学习“C++免费学习笔记(深入)”;
std::vectorpool(10000); std::iota(pool.begin(), pool.end(), 1); // 填 1~10000 std::vector result(5); std::sample(pool.begin(), pool.end(), result.begin(), 5, std::mt19937{std::random_device{}()});
-
std::sample内部使用 reservoir sampling 或类似策略,时间复杂度 O(n),空间只占输出大小 - 若编译器不支持 C++17,退回到先建小 pool(如 1~100)再 shuffle,或手动实现带 map 查重的抽取(但仅当 n 极小才划算)
- 注意
std::sample不保证结果有序,如需升序得额外调std::sort
为什么不用 std::rand() % N 加 set 去重?
看似简单,实际在中等规模(比如抽 50 个不重复数,范围 0~99)时,碰撞概率陡增,性能崩塌。数学上期望尝试次数是调和级数,抽 k 个数在 N 范围内平均要调用 rand 约 N·H_k ≈ N·(ln k + γ) 次;k 接近 N 时,后期几乎每次都在撞。
- 更严重的是
std::rand()本身周期短、低位比特相关性高,%运算会放大偏差,导致某些数永远比别的数更容易被选中 - set 插入 + 查重带来 O(k log k) 额外开销,而
std::shuffle是 O(k) 时间(只 shuffle 前 k 位即可)+ O(k) 空间 - 没有线程安全性:
std::rand()是全局状态,多线程并发调用未定义行为;std::mt19937实例是局部对象,天然安全
真正需要警惕的,是把“能跑通”当成“正确”——尤其在测试样本小的时候,偏差根本看不出来。










