为何rand() % N在小分布场景下可满足均匀随机数生成要求?
小范围随机数取模无偏差的原理说明
先明确模偏差的本质
我们常说不要直接对随机数生成器输出取模,核心原因是模偏差:
假设随机数生成器的输出范围是闭区间 [0, M],总共有 M+1 个可能的取值,我们要映射到目标范围 [0, n-1]。如果 M+1 不能被 n 整除,那么余数为 0 到 (M+1) % n -1 的值,出现的概率会比其他余数高1/M+1,分布就不均匀了。
你碰到的现象为什么成立
你用的是std::mt19937,这是32位梅森旋转生成器,输出范围固定是 [0, 2^32 - 1],总共有2^32个可能取值。你要映射到的目标范围是0~15,也就是n=16,刚好2^32 % 16 = 0,所有余数的出现次数完全相等,根本不存在模偏差。
而std::uniform_int_distribution的核心逻辑是处理非整除的场景:当M+1不是n的整数倍时,它会用拒绝采样扔掉超出整数倍范围的随机数,保证分布均匀。当刚好整除时,它的内部实现本质就是直接取模,所以你两次运行的结果完全一致。
你给出的测试代码如下:
std::mt19937 gen; gen.seed(0); int ROWS = 6; int COLS = 10; std::cout << "mod: \n"; for (size_t i = 0; i < ROWS; ++i){ for (size_t j = 0; j < COLS; ++j){ std::cout << std::setw(2) << gen() % 16 << " "; } std::cout << "\n"; } std::cout << "\n"; gen.seed(0); std::uniform_int_distribution<> distrib(0, 15); std::cout << "dist: \n"; for (size_t i = 0; i < ROWS; ++i){ for (size_t j = 0; j < COLS; ++j){ std::cout << std::setw(2) << distrib(gen) << " "; } std::cout << "\n"; }
运行输出两者完全一致,就是上述原理的直接体现:
mod: 12 15 5 0 3 11 3 7 9 3 5 2 4 7 6 8 8 12 10 1 6 7 7 14 8 1 5 9 13 8 9 4 3 0 3 5 14 15 15 0 2 3 8 1 3 13 3 3 14 7 0 1 9 9 15 0 15 10 4 7 dist: 12 15 5 0 3 11 3 7 9 3 5 2 4 7 6 8 8 12 10 1 6 7 7 14 8 1 5 9 13 8 9 4 3 0 3 5 14 15 15 0 2 3 8 1 3 13 3 3 14 7 0 1 9 9 15 0 15 10 4 7
针对你的疑问的解答
- 这个现象和2字节范围没有关系:核心判断条件是随机数生成器的总输出个数是否是目标范围的整数倍,和范围本身的大小没有直接关系。比如就算你目标范围是0~65535(2字节),
2^32是65536的整数倍,直接取模也完全均匀。 - 不是只要范围足够小取模就一定均匀:举个反例,目标范围是0~2(n=3),哪怕范围很小,
2^32 %3 =1,余数0和1的出现次数会比余数2多一次,存在微小的模偏差,只是偏差量级极低,普通测试很难感知到。 - 不存在所谓2字节的阈值:你听说的2字节阈值大概率是针对旧的
rand()实现的,很多系统的rand()输出范围是[0, 32767](总个数32768),当目标范围小于256时,32768 %n的余数占总样本的比例不到1/128,偏差非常小,很容易被忽略,但本质还是存在偏差。
内容的提问来源于stack exchange,提问作者Trevor Hickey
相关产品推荐
相关产品推荐

