1. 引言
在编程的世界中,随机性(Randomness)经常被提及。无论是为了测试、模拟还是为了增加某种不可预测性,随机数都是我们日常工作中的重要工具。但是,真正理解随机数的生成和分布需要深入到计算机科学和数学的交叉领域。在这一章中,我们将探讨C++标准库中的随机数生成与分布,并从心理学的角度来看待它们。
1.1 C++标准库中的随机数生成与分布
在C++中,随机数的生成不仅仅是调用一个函数那么简单。它涉及到一系列的生成器和分布,这些都在头文件中定义。这个库提供了一种灵活而强大的方式来生成随机数,满足了各种应用的需求。
例如,当我们想要生成一个随机整数时,我们可能会这样做:
#include <random> std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> distr(1, 6); int random_number = distr(gen);
在上面的代码中,我们首先创建了一个随机设备rd
,然后使用它来初始化一个Mersenne Twister生成器gen
。接下来,我们定义了一个均匀分布distr
,范围从1到6,最后我们使用这个分布来生成一个随机整数。
但为什么我们需要这么多步骤来生成一个随机数呢?这背后的原因与心理学中的一个观点相吻合:人们对于随机性的直觉往往是错误的。我们很难真正理解随机性,而计算机在生成随机数时也面临着相似的挑战。为了获得真正的随机性,我们需要使用复杂的算法和方法。
1.2 随机数分布的重要性
当我们谈论随机数时,我们实际上是在谈论两件事:随机数的生成和随机数的分布。生成是关于如何产生随机数的,而分布是关于这些数字如何分布的。
想象一下,如果你在一个骰子游戏中使用一个不均匀的骰子,那么某些数字可能会比其他数字出现得更频繁。这就是为什么我们需要考虑分布的原因。在C++中,我们有多种分布可以选择,如均匀分布、正态分布等。
从心理学的角度看,人们对于随机事件的期望往往是均匀分布的。但实际上,许多随机事件遵循其他类型的分布。例如,人们的身高遵循正态分布,而不是均匀分布。因此,了解不同的分布类型可以帮助我们更好地模拟和理解现实世界。
“随机性不是不按某种顺序,而是按照我们不能确定的顺序。” - 西尼加·塔列布(Nassim Nicholas Taleb)
2. C++随机数生成器简介
在深入探讨随机数分布之前,我们首先需要理解随机数是如何在C++中生成的。这不仅仅是一个简单的过程,而是涉及到一系列的生成器和算法。
2.1 基本的随机数生成器
在C++中,随机数生成器是用于产生随机序列的对象。这些生成器基于特定的算法,可以产生伪随机数,这意味着它们实际上是由算法确定的,但看起来像是随机的。
以下是一些常见的随机数生成器:
std::default_random_engine
: 默认的随机数生成器。std::mt19937
: 基于Mersenne Twister算法的32位随机数生成器。std::mt19937_64
: 基于Mersenne Twister算法的64位随机数生成器。std::ranlux24
和std::ranlux48
: 基于RANLUX算法的随机数生成器。
每种生成器都有其特点和应用场景。例如,Mersenne Twister生成器被广泛认为是高质量的随机数生成器,但它的状态非常大,可能不适合内存受限的环境。
2.2 为什么需要分布?
你可能会问,为什么我们不能直接使用随机数生成器产生的数值呢?答案是,生成器产生的数值通常是在一个非常大的范围内,例如从0到4,294,967,295。而我们通常需要的随机数是在一个特定的范围内,例如从1到6。这就是分布发挥作用的地方。
分布可以看作是一个函数,它接受生成器产生的原始随机数,并将其转换为我们需要的范围和形状。例如,均匀分布会确保每个数值在指定范围内都有相同的概率被选中。
“在混乱中寻找模式,是人类的天性。” - 卡尔·塞根(Carl Sagan)
从心理学的角度看,我们的大脑总是试图找到模式和规律,即使在随机数据中也是如此。这就是为什么我们需要确保我们的随机数真的是随机的,而不是某种隐藏的模式。
2.3 从底层源码看随机数生成
让我们深入探讨一下随机数生成器是如何工作的。以Mersenne Twister为例,它是一个基于线性反馈移位寄存器的算法。它的工作原理是使用一系列的位操作和模运算来产生随机数。
方法名称 | 功能描述 | 返回值 |
seed() |
设置随机数生成器的种子 | 无 |
operator() |
生成一个随机数 | 随机数 |
min() |
返回生成器可以产生的最小值 | 最小值 |
max() |
返回生成器可以产生的最大值 | 最大值 |
当我们调用operator()
方法时,生成器会使用其内部状态和算法来产生一个新的随机数。这个数值通常是在一个非常大的范围内,所以我们需要使用分布来将其转换为我们需要的范围。
3. 均匀分布
均匀分布是最基础的随机数分布,它确保在指定的范围内每个数值都有相同的出现概率。在C++标准库中,我们主要使用std::uniform_int_distribution
和 std::uniform_real_distribution
来实现整数和浮点数的均匀分布。
3.1 std::uniform_int_distribution
和 std::uniform_real_distribution
的基本介绍
3.1.1 std::uniform_int_distribution
这个分布用于生成一个在指定闭区间 [a, b] 内的整数值。例如,如果我们想生成一个在 [1, 6] 之间的整数,可以想象这就像掷一个六面的骰子。
示例:
#include <random> #include <iostream> int main() { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> distr(1, 6); // 定义在 [1, 6] 之间的分布 for (int i = 0; i < 10; ++i) { std::cout << distr(gen) << ' '; // 生成并打印10个随机数 } return 0; }
在这个示例中,我们使用了Mersenne Twister算法 (std::mt19937
) 作为随机数生成器。这是一个广泛使用的高质量的随机数生成器。
3.1.2 std::uniform_real_distribution
这个分布用于生成一个在指定半开区间 [a, b) 内的浮点数值。这意味着生成的数值可以等于 a 但永远不会达到 b。
示例:
#include <random> #include <iostream> int main() { std::random_device rd; std::mt19937 gen(rd()); std::uniform_real_distribution<> distr(0.0, 1.0); // 定义在 [0.0, 1.0) 之间的分布 for (int i = 0; i < 10; ++i) { std::cout << distr(gen) << ' '; // 生成并打印10个随机数 } return 0; }
从心理学的角度来看,人们对于随机性的直觉往往是有偏见的。例如,当我们掷一个公正的骰子时,我们可能期望每个数字在长时间内出现的次数大致相同。但在短时间内,可能会出现连续掷出相同数字的情况。这并不意味着骰子是有偏的或者随机数生成器是有问题的,这只是随机性的本质。正如心理学家Daniel Kahneman在《思考,快与慢》(Thinking, Fast and Slow)中所说,人们对随机性的直觉往往是错误的。
3.2 使用场景与示例
均匀分布在许多应用中都非常有用。例如,模拟骰子掷出的结果、生成随机颜色代码或在游戏中随机选择一个玩家。
示例:
想象一个简单的游戏,玩家需要猜测计算机生成的1到10之间的数字。我们可以使用std::uniform_int_distribution
来生成这个数字。
#include <random> #include <iostream> int main() { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> distr(1, 10); int secretNumber = distr(gen); int guess; std::cout << "猜测一个1到10之间的数字: "; std::cin >> guess; if (guess == secretNumber) { std::cout << "恭喜你,猜对了!"; } else { std::cout << "很遗憾,正确的数字是 " << secretNumber << "."; } return 0; }
3.3 注意事项
- 选择合适的随机数生成器:虽然C++提供了多种随机数生成器,但不是所有的生成器都适合所有的应用。例如,
std::mt19937
是一个高质量的随机数生成器,但它可能比其他简单的生成器慢。 - 初始化随机数生成器:使用
std::random_device
或当前时间来初始化随机数生成器,确保每次运行程序时都能得到不同的随机数序列。 - 理解分布的范围:确保你理解了所选分布的范围,特别是
std::uniform_real_distribution
的半开区间特性。 - 避免偏见:正如前面提到的,人们对随机性的直觉可能是有偏见的。当使用随机数时,要确保理解随机性的本质,并避免因短期的不规律性而得出错误的结论。
在深入探讨随机数分布的实现时,我们可以查看C++标准库的源代码。这可以帮助我们更好地理解这些分布是如何工作的,以及它们是如何确保生成的数字确实是随机的。
4. 正态分布 (std::normal_distribution
)
正态分布(Normal Distribution),也被称为高斯分布(Gaussian Distribution),是自然和社会科学中最常见的概率分布之一。在C++中,我们可以使用std::normal_distribution
来生成符合正态分布的随机数。
4.1 原理与特点
正态分布是一个连续的概率分布,其函数形状为一个对称的钟形曲线。正态分布的两个主要参数是均值(mean)和标准差(standard deviation)。均值描述了分布的中心位置,而标准差描述了分布的宽度或数据的离散程度。
从心理学的角度来看,正态分布描述了许多自然现象的随机变化,如人的智商、身高或考试分数。这是因为许多独立的因素影响了这些测量,当这些因素叠加在一起时,它们形成了一个正态分布。
“在自然中,我们经常看到正态分布,因为它是中心极限定理的一个结果。” - Sir Francis Galton
4.1.1 正态分布的数学形式
正态分布的概率密度函数(Probability Density Function, PDF)为:
[ f(x) = \frac{1}{\sigma \sqrt{2\pi}} e{-\frac{(x-\mu)2}{2\sigma^2}} ]
其中,\(\mu\)
是均值,\(\sigma\)
是标准差。
4.2 使用场景与示例
考虑一个场景,你正在为一个游戏设计一个角色的属性,如力量、智慧和敏捷性。你希望大多数角色的属性都是平均的,但也有一些角色的属性非常高或非常低。这是一个典型的正态分布的应用场景。
#include <iostream> #include <random> int main() { std::random_device rd; std::mt19937 gen(rd()); std::normal_distribution<> d(50, 10); // 均值为50,标准差为10 for (int i = 0; i < 10; ++i) { std::cout << d(gen) << "\n"; // 生成符合正态分布的随机数 } return 0; }
在上述代码中,我们使用了均值为50、标准差为10的正态分布来生成角色的某一属性。大多数生成的值将接近50,但也有可能生成40以下或60以上的值。
4.3 注意事项
- 当使用
std::normal_distribution
时,确保你理解了均值和标准差的意义,并根据你的应用场景正确地设置它们。 - 正态分布生成的值是无界的,这意味着理论上它可以生成任何浮点数。在实际应用中,你可能需要对生成的值进行裁剪或转换,以确保它们在一个合理的范围内。
“大多数人都是平均的,这是正态分布的美妙之处。” - Carl Friedrich Gauss
方法 | 描述 | 适用场景 |
std::uniform_int_distribution |
生成均匀分布的整数 | 当所有值的概率都相等时 |
std::uniform_real_distribution |
生成均匀分布的浮点数 | 当所有值的概率都相等时 |
std::normal_distribution |
生成正态分布的浮点数 | 当大多数值围绕一个中心值聚集时 |
从底层源码的角度来看,std::normal_distribution
使用了Box-Muller变换或其他方法来从均匀分布的随机数生成正态分布的随机数。
5. 伯努利与二项分布
在我们的日常生活中,许多事件都有两种可能的结果。例如,当我们抛硬币时,结果可能是正面或反面。这种只有两种可能结果的随机试验被称为伯努利试验 (Bernoulli trial)。在C++中,我们可以使用 std::bernoulli_distribution
和 std::binomial_distribution
来模拟这种试验。
5.1 std::bernoulli_distribution (伯努利分布)
伯努利分布描述了一个试验只有两种可能结果的情况。例如,抛硬币的正面或反面。
示例:
#include <iostream> #include <random> int main() { std::default_random_engine generator; std::bernoulli_distribution distribution(0.5); // 0.5 的概率为 true,0.5 的概率为 false int count_true = 0; for (int i = 0; i < 1000; ++i) { if (distribution(generator)) { count_true++; } } std::cout << "Number of true: " << count_true << std::endl; }
在上述代码中,我们模拟了1000次抛硬币的试验,并统计了正面出现的次数。
从心理学的角度来看,人们对于50-50的结果往往有一种直观的公平感。这也是为什么我们经常用抛硬币来做决策。正如心理学家Daniel Kahneman在《思考,快与慢》(Thinking, Fast and Slow) 中所说,人们对于随机事件的结果往往有一种固有的期望。
5.2 std::binomial_distribution (二项分布)
二项分布描述了在给定次数的独立伯努利试验中成功的次数。例如,抛10次硬币得到正面的次数。
示例:
#include <iostream> #include <random> int main() { std::default_random_engine generator; std::binomial_distribution<int> distribution(10, 0.5); // 10次试验,每次试验成功的概率为0.5 int count_success = distribution(generator); std::cout << "Number of successes in 10 trials: " << count_success << std::endl; }
在上述代码中,我们模拟了10次抛硬币的试验,并统计了10次中正面出现的次数。
从心理学的角度来看,人们对于连续的随机事件结果往往有一种连续性的偏见。例如,如果一个人连续抛出了几次正面,他可能会错误地认为下一次抛出反面的概率会增加。这种现象在心理学中被称为赌徒谬误 (Gambler’s Fallacy)。
5.2.1 伯努利分布与二项分布的关系
伯努利分布是二项分布的一个特例,当试验次数为1时,二项分布就退化为伯努利分布。
分布类型 | 试验次数 | 成功概率 | 示例 |
伯努利分布 | 1 | p | 抛一次硬币 |
二项分布 | n | p | 抛n次硬币 |
从底层源码的角度来看,std::bernoulli_distribution
和 std::binomial_distribution
都是基于随机数生成器来实现的。但它们的实现细节和算法可能会有所不同,这取决于C++标准库的具体实现。
【C++ 随机数分布类型 】深入探索C++随机数分布:原理、应用与实践(二)https://developer.aliyun.com/article/1467679