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_distributionstd::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是「实现定义」的,不同平台可能用不同的算法。所以,我建议你总是显式指定引擎类型,别偷懒。

知识体系一览

下面这张图帮你理清随机数体系的整体结构:

C++ STL 随机数体系 随机数引擎(Engine) 负责生成原始伪随机比特流 mt19937(梅森旋转) minstd_rand(线性同余) ranlux48(滞后斐波那契) 随机数分布(Distribution) 将原始比特转换为特定分布的随机数 uniform_int_distribution uniform_real_distribution normal_distribution bernoulli_distribution 随机数生成器(Engine + Distribution)

从这张图可以看得很清楚:引擎在最底层,负责「出力」;分布器在中间层,负责「塑形」;而我们在代码中实际使用的,是引擎和分布器的组合——也就是一个完整的随机数生成器。

总结一下

STL的随机数设施,说白了就是「引擎负责生成,分布器负责整形」。这种分离设计让代码更灵活、更可维护。你可以在不改变分布逻辑的情况下更换引擎,也可以在不改变引擎的情况下更换分布器。

嗯,最后提醒一句:rand()该退休了。从C++11开始,请用<random>头文件里的这套设施。你的代码会更健壮,你的随机数会更「随机」。

我的个人建议:std::mt19937std::random_device作为默认组合。除非你有特殊需求(比如需要可重复的随机序列用于测试),否则这个组合能覆盖90%以上的场景。

公众号:蓝海资料掘金营,微信deep3321