不能用 rand() % n 因分布不均且 rand() 低位质量差;应使用 std::uniform_int_distribution 配 std::mt19937,或更推荐 std::shuffle 实现 Fisher–Yates 洗牌。

为什么不能用 rand() % n 做随机索引
直接用 rand() % n 生成下标看似简单,但会导致分布严重不均——尤其当 RAND_MAX + 1 不能被 n 整除时,小余数对应的索引会多出现一次。比如 RAND_MAX == 32767(常见于旧 libc),对 n == 10000,索引 0 到 2767 的概率比 2768 到 9999 高约 1.3 倍。
更糟的是:rand() 在多数实现中周期短、低位比特相关性强,% 操作还会放大这种缺陷。
- 改用
std::uniform_int_distribution<int>(0, n-1)</int>配合std::mt19937 - 种子必须只初始化一次,别在循环里反复用
std::random_device{}构造器(可能返回相同值) - 若需可重现结果,显式传入固定种子,如
std::mt19937 gen{42}
用 std::shuffle 最安全也最省事
C++11 起标准库已提供正确实现的 Fisher–Yates(现代版)洗牌,底层自动处理分布均匀性和迭代器适配。
它要求容器支持随机访问迭代器,所以原生数组、std::array、std::vector 都能直接用;std::list 不行。
立即学习“C++免费学习笔记(深入)”;
int arr[] = {1, 2, 3, 4, 5};
std::mt19937 g{std::random_device{}()};
std::shuffle(std::begin(arr), std::end(arr), g);
- 注意:
std::shuffle第三个参数是**随机数生成器对象**,不是分布对象 - 别传
std::default_random_engine——它不保证跨平台一致性;std::mt19937是稳妥选择 - 对
std::vector<T>,确保T支持移动或拷贝;若含不可移动资源,需自定义交换逻辑
手动实现 Fisher–Yates 时最容易错在哪
手写时核心陷阱是循环边界和随机范围写反:必须从末尾向前,每次随机选 [0, i] 范围内的索引(含当前位),再与 i 交换。写成 [0, i) 或从头向后扫都会破坏均匀性。
// ✅ 正确:i 从 n-1 递减到 1,j ∈ [0, i]
for (int i = n - 1; i > 0; --i) {
int j = dist(eng) % (i + 1); // 注意是 i+1,不是 i
std::swap(arr[i], arr[j]);
}
- 错误模式:
for (int i = 0; i → 产生偏置,高位元素更难被换到前面 - 错误模式:
j = dist(eng) % i(当i == 0时除零,且漏掉自身交换机会) - 若用指针操作数组,务必确认
arr + i和arr + j在合法范围内,别越界
性能和兼容性要注意的点
对小数组(n < 16),std::shuffle 内部可能有优化路径;但手动实现几乎没额外开销。真正影响性能的是随机数生成器——std::mt19937 初始化稍重,但单次 shuffle 中生成 n 个随机数很快。
- Windows MSVC 的
std::shuffle在 Debug 模式下可能校验迭代器有效性,带来可观开销;Release 下无此问题 - 嵌入式环境若禁用
<random>,可用 xorshift 等轻量 PRNG 替代,但务必验证其周期和分布质量 - 若数组元素是 POD 类型且对缓存敏感,考虑用
std::span<T>(C++20)包装,避免拷贝整个容器
真正容易被忽略的是:洗牌后若需保留原始顺序做对比,得提前深拷贝——尤其是数组指针指向堆内存时,别只复制指针。


















