std::shuffle打乱容器再取前N个比手写随机去重更可靠;应使用std::mt19937配合std::random_device初始化,避免rand()及模运算;大范围小样本优先用C++17的std::sample。

用 std::shuffle 打乱容器比手写随机去重更可靠
直接生成“不重复的随机数”本质是采样问题,硬靠 rand() + 循环检查重复,容易卡死或逻辑错。C++11 起标准库提供了更安全的做法:先构造完整序列,再用 std::shuffle 打乱,取前 N 个即可。
常见错误现象:while (set.count(x)) x = rand() % max; 在 max 接近所需数量时,碰撞概率陡增,性能暴跌;且 rand() 周期短、分布差,std::mt19937 才是现代选择。
- 必须用
std::random_device初始化std::mt19937,不能只用time(0)当种子 -
std::shuffle要求迭代器支持随机访问(std::vector可以,std::list不行) - 若只需少量不重复数(比如从 1~1000 里取 5 个),用
std::sample(C++17)更省内存,但兼容性稍弱
std::vector<int> v(100);
std::iota(v.begin(), v.end(), 1); // 1~100
std::shuffle(v.begin(), v.end(), std::mt19937{std::random_device{}()});
std::vector<int> result(v.begin(), v.begin() + 10); // 前10个
std::sample 在 C++17 中解决“大范围小样本”场景
当你要从 1 到 1e9 的整数中随机取 10 个不重复值,建 1e9 元素的 vector 显然不可行。std::sample 是为此设计的——它用蓄水池算法,空间复杂度 O(k),时间复杂度 O(n),且不依赖容器是否可随机访问。
使用场景:抽奖 ID、日志采样、测试数据生成等“范围极大但取数极少”的情况。
立即学习“C++免费学习笔记(深入)”;
- 第三个参数是输出迭代器,必须保证目标容器有足够空间(或用
std::back_inserter) - 第四个参数是样本数量,不能超过输入范围长度(否则行为未定义)
- 底层仍需靠谱的随机引擎,
std::mt19937是底线,别用rand()
std::vector<int> out(10);
std::sample(v.begin(), v.end(), out.begin(), 10, std::mt19937{std::random_device{}()});
Windows 下 RAND_MAX 只有 32767,别用 rand() % N
这是最隐蔽的坑:rand() 返回值范围由 RAND_MAX 决定,MSVC 默认是 15 位,导致 rand() % 100000 实际只有 32767 个可能值,且低比特偏差严重。即使你没报错,结果也根本不是均匀分布。
参数差异:std::uniform_int_distribution<int>(a, b) 会自动处理边界和模偏差,而 % 操作不会。
- 永远不要对
rand()做模运算来缩放范围 - 如果必须用 C 风格函数,改用
rand_s()(Windows)或arc4random_uniform()(macOS/BSD) -
std::uniform_int_distribution构造一次后可复用,不要每次调都重建
打乱算法不是万能的——注意 std::shuffle 的“真随机”前提
std::shuffle 本身是 Fisher–Yates 算法,数学上无偏,但它的随机性完全取决于传入的引擎。如果你用固定种子(比如 std::mt19937{12345}),每次运行结果都一样;如果用 std::random_device 但系统不支持真熵(某些嵌入式或容器环境),可能退化为常量。
容易踩的坑:在单元测试里用 std::random_device,CI 环境却返回全 0,导致测试偶然失败。
- 调试时可临时固定种子(如
std::mt19937{42}),但上线必须换回std::random_device - Linux 上
/dev/urandom可用,但某些精简版容器镜像会挂载空设备,std::random_device::entropy()返回 0.0 就该报警 - 打乱前确认容器 size > 0,否则
shuffle(begin, end, ...)行为未定义











