67、STL与随机数:随机数引擎、随机数分布、随机数生成器
说到随机数,很多C++老手第一反应还是rand()和srand()。嗯,我承认,早年我自己也是这么写的。直到有一次在项目中做蒙特卡洛模拟,发现rand()的周期太短,分布质量也堪忧,结果模拟出来的数据明显有规律可循——那叫一个尴尬。
从C++11开始,STL引入了一套全新的随机数设施。说白了,就是把「随机数怎么产生」和「产生什么样的随机数」这两件事彻底分开了。你想想看,这设计多清爽:引擎只管吐原始比特,分布器负责把这些比特变成你想要的形状。
核心三件套:随机数引擎(Engine)、随机数分布(Distribution)、随机数生成器(Generator,即引擎+分布的封装)。
随机数引擎:底层的「搅碎机」
引擎是随机数体系的基石。它负责维护内部状态,并根据算法生成伪随机数序列。STL提供了几种常用引擎:
| 引擎名称 | 特点 | 适用场景 |
|---|---|---|
std::mt19937 |
梅森旋转算法,周期长达2^19937-1 | 通用场景,我最常用这个 |
std::minstd_rand |
线性同余,速度快但周期短 | 嵌入式或对性能要求极高的场景 |
std::ranlux48 |
滞后斐波那契,质量高但慢 | 需要高质量随机数的科学计算 |
我个人习惯用std::mt19937。为什么?因为它在速度和随机性质量之间取得了很好的平衡。我在项目中做过测试,mt19937生成一亿个随机数也就一两秒的事,完全够用。
小技巧:引擎需要种子(seed)来初始化。别用固定种子,除非你是在调试。我一般用std::random_device来获取真随机数作为种子。
随机数分布:把比特变成你想要的形状
引擎吐出来的是一堆无符号整数,但实际应用中我们需要各种分布的随机数。分布器就是干这个的。
常用的分布器有这些:
- 均匀分布:
std::uniform_int_distribution和std::uniform_real_distribution。最常用,没有之一。 - 正态分布:
std::normal_distribution。模拟自然现象、误差分析时必备。 - 伯努利分布:
std::bernoulli_distribution。模拟抛硬币、开关事件。 - 泊松分布:
std::poisson_distribution。模拟单位时间内事件发生的次数。
我曾经在做一个网络延迟模拟器时,需要生成符合正态分布的延迟值。用std::normal_distribution配合mt19937,几行代码就搞定了。要是用rand()自己实现正态分布?那代码量至少翻三倍,还容易出bug。
实战:一个完整的随机数生成器
下面是一个完整的例子,展示了如何生成各种分布的随机数:
#include <iostream>
#include <random>
#include <vector>
#include <algorithm>
int main() {
// 1. 创建引擎并初始化种子
std::random_device rd;
std::mt19937 gen(rd());
// 2. 创建分布器
std::uniform_int_distribution<int> dis_int(1, 100);
std::uniform_real_distribution<double> dis_real(0.0, 1.0);
std::normal_distribution<double> dis_normal(50.0, 10.0); // 均值50,标准差10
// 3. 生成随机数
std::cout << "均匀整数(1-100):" << dis_int(gen) << std::endl;
std::cout << "均匀浮点数(0-1):" << dis_real(gen) << std::endl;
std::cout << "正态分布(均值50):" << dis_normal(gen) << std::endl;
// 4. 批量生成并统计
std::vector<double> samples;
for (int i = 0; i < 10000; ++i) {
samples.push_back(dis_normal(gen));
}
// 计算均值和标准差
double sum = std::accumulate(samples.begin(), samples.end(), 0.0);
double mean = sum / samples.size();
std::cout << "实际均值:" << mean << std::endl;
return 0;
}
注意:每次调用分布器时都要传入引擎对象。不要复用同一个分布器对象去生成不同分布的随机数——每个分布器内部有自己的状态,混用会导致结果异常。
避坑指南:我踩过的那些坑
我曾经在写一个多线程程序时,让每个线程共享同一个引擎对象。结果发现生成的随机数序列高度相关,完全失去了随机性。后来才意识到,引擎不是线程安全的,每个线程必须有自己的引擎实例。
还有一次,我为了图方便,直接用std::default_random_engine。结果在不同编译器上跑出来的结果完全不一样。为什么?因为default_random_engine是「实现定义」的,不同平台可能用不同的算法。所以,我建议你总是显式指定引擎类型,别偷懒。
知识体系一览
下面这张图帮你理清随机数体系的整体结构:
从这张图可以看得很清楚:引擎在最底层,负责「出力」;分布器在中间层,负责「塑形」;而我们在代码中实际使用的,是引擎和分布器的组合——也就是一个完整的随机数生成器。
总结一下
STL的随机数设施,说白了就是「引擎负责生成,分布器负责整形」。这种分离设计让代码更灵活、更可维护。你可以在不改变分布逻辑的情况下更换引擎,也可以在不改变引擎的情况下更换分布器。
嗯,最后提醒一句:rand()该退休了。从C++11开始,请用<random>头文件里的这套设施。你的代码会更健壮,你的随机数会更「随机」。
我的个人建议:把std::mt19937和std::random_device作为默认组合。除非你有特殊需求(比如需要可重复的随机序列用于测试),否则这个组合能覆盖90%以上的场景。