【C++ 随机数分布类型 】深入探索C++随机数分布:原理、应用与实践(二)

简介: 【C++ 随机数分布类型 】深入探索C++随机数分布:原理、应用与实践

【C++ 随机数分布类型 】深入探索C++随机数分布:原理、应用与实践(一)https://developer.aliyun.com/article/1467678


6. 几何与泊松分布

在我们的日常生活中,许多事件的发生都遵循某种概率分布。理解这些分布对于编程和数据分析至关重要。今天,我们将深入探讨两种常见的概率分布:几何分布和泊松分布,并从心理学的角度来看它们如何影响我们的决策。

6.1 几何分布 (std::geometric_distribution)

几何分布(Geometric Distribution)描述了在第一次成功之前需要进行的伯努利试验的次数。例如,如果你连续抛硬币,直到第一次得到正面,那么抛硬币的次数就遵循几何分布。

原理

考虑一个伯努利试验,成功的概率为 p。那么,第一次成功发生在第 k 次试验的概率为 (1-p)^(k-1) * p

从心理学的角度看,人们对于连续失败后的第一次成功通常有很强的期望。这种期望可能会导致人们在面对困难时更加坚持,因为他们相信成功即将到来。

示例

#include <iostream>
#include <random>
int main() {
    std::default_random_engine generator;
    std::geometric_distribution<int> distribution(0.5); // p=0.5
    int count = 0;
    for (int i=0; i<1000; ++i) {
        if (distribution(generator) == 1) {
            count++;
        }
    }
    std::cout << "Number of first-time successes in 1000 trials: " << count << std::endl;
}

在上述代码中,我们模拟了1000次试验,每次试验的成功概率为0.5。我们统计了第一次成功的次数。

6.2 泊松分布 (std::poisson_distribution)

泊松分布(Poisson Distribution)描述了在固定时间间隔或空间内随机事件发生的次数。这种分布在描述稀有事件时特别有用。

原理

考虑一个事件在单位时间内发生的平均次数为 λ。那么,该事件在单位时间内发生 k 次的概率为 (e^(-λ) * λ^k) / k!

心理学家 Daniel Kahneman 在其著作《Thinking, Fast and Slow》(《快思慢想》)中提到,人们往往高估稀有事件的发生概率。这种认知偏差可能是由于稀有事件的显著性和记忆中的可用性所导致的。

示例

#include <iostream>
#include <random>
int main() {
    std::default_random_engine generator;
    std::poisson_distribution<int> distribution(3); // λ=3
    int count = 0;
    for (int i=0; i<1000; ++i) {
        if (distribution(generator) == 2) {
            count++;
        }
    }
    std::cout << "Number of times the event occurred twice in 1000 units of time: " << count << std::endl;
}

在上述代码中,我们模拟了1000个单位时间,每个单位时间内事件的平均发生次数为3。我们统计了事件在单位时间内发生两次的次数。

方法对比

分布类型 参数 描述 示例
几何分布 p (成功概率) 描述在第一次成功之前的试验次数 抛硬币直到得到正面
泊松分布 λ (单位时间内的平均事件数) 描述在固定时间间隔内的事件发生次数 一小时内到达的电话数量

7. 指数分布 (std::exponential_distribution)

指数分布是描述两个连续随机事件之间的时间间隔的概率分布。在现实生活中,我们经常遇到这样的情况,例如:一个呼叫中心接到两个连续电话之间的时间间隔,或者公交车站两辆连续公交车之间的到达时间。

7.1 原理与特点

指数分布的一个关键特性是它具有所谓的“无记忆性”(memorylessness)。这意味着,无论我们已经等待了多长时间,下一个事件发生的期望时间始终保持不变。从心理学的角度看,这与人们对于等待的感知是一致的。例如,当我们在等待公交车时,无论我们已经等待了多长时间,我们总是觉得下一辆公交车可能马上就要到来。

在C++中,std::exponential_distribution 是用来模拟这种分布的。它的构造函数接受一个参数λ(lambda),这个参数是事件的平均发生率。例如,如果一个呼叫中心平均每分钟接到一个电话,那么λ就是1。

std::default_random_engine generator;
std::exponential_distribution<double> distribution(1.0);  // λ = 1.0
double interval = distribution(generator);

上面的代码会生成一个表示两个连续电话之间的时间间隔的随机数。

7.2 使用场景与示例

考虑一个常见的场景:一个服务器处理客户端的请求。如果我们知道这个服务器平均每秒处理10个请求,我们可以使用指数分布来模拟两个连续请求之间的时间间隔。

std::default_random_engine generator;
std::exponential_distribution<double> distribution(10.0);  // λ = 10.0
double interval = distribution(generator);

这里,interval 表示两个连续请求之间的时间间隔。

从心理学的角度看,这种模拟可以帮助我们更好地理解用户的等待体验。如果用户经常需要等待很长时间,他们可能会变得不耐烦。通过模拟,我们可以预测并优化这种等待体验。

7.3 注意事项

  1. 当使用 std::exponential_distribution 时,确保λ值是正的。λ值代表了事件的平均发生率,它不能是负数或零。
  2. 指数分布是一个连续的分布,这意味着它可以生成任何在0到正无穷之间的值。但在实际应用中,你可能需要对这些值进行取整或限制。

7.3.1 深入源码

std::exponential_distribution 的实现基于逆变换采样方法。这种方法的基本思想是先生成一个均匀分布的随机数,然后通过某个函数将其转换为所需的分布。这种方法的优点是速度快,但需要确保使用的函数是单调的。

7.4 技术对比

分布类型 特点 使用场景 参数
均匀分布 (std::uniform_distribution) 生成在指定范围内的随机数 任何需要均匀随机数的场景 范围的最小值和最大值
指数分布 (std::exponential_distribution) 描述两个连续事件之间的时间间隔 事件的时间间隔,如呼叫中心的电话间隔 事件的平均发生率λ

在选择分布时,重要的是要考虑你的具体需求。不同的分布有不同的特点和使用场景,选择合适的分布可以使你的模拟或应用更加准确。

8. 自定义分布

在C++的世界中,虽然标准库为我们提供了丰富的随机数分布,但有时我们可能需要根据特定的需求创建自己的随机数分布。这不仅是一个技术挑战,更是一个对人性的挑战。正如心理学家Carl Rogers所说:“我们不能改变、我们不能超越我们所不知道的。”(我们不能改变或超越我们所不知道的事物)。因此,深入理解如何从底层创建自定义分布是至关重要的。

8.1 为什么需要自定义分布?

在某些特定的应用场景中,标准库提供的分布可能不满足我们的需求。例如,可能需要一个特定形状的分布,或者需要模拟某种现实世界的随机过程。在这种情况下,我们需要自定义分布来满足这些特定的需求。

8.2 创建自定义分布的基本步骤

  1. 定义分布的形状:首先,需要确定分布的形状。这通常涉及到数学和统计学的知识。
  2. 实现分布函数:使用C++代码实现分布函数。这可能涉及到一些数学计算和算法。
  3. 测试和验证:确保自定义分布的实现是正确的,并且满足预期的需求。

8.3 示例:三角形分布 (Triangular Distribution)

三角形分布是一个简单的分布,它在最小值和最大值之间呈三角形。这种分布在某些模拟和统计分析中很有用。

8.3.1 三角形分布的原理

三角形分布的概率密度函数 (PDF, 概率密度函数) 可以表示为:


image.png

image.png

其中,(a) 是最小值,(b) 是最大值,(c) 是峰值所在的位置。

8.3.2 C++实现

class TriangularDistribution {
private:
    double a, b, c;
public:
    TriangularDistribution(double min, double max, double peak) : a(min), b(max), c(peak) {}
    double operator()(std::mt19937& gen) {
        std::uniform_real_distribution<> distr(0.0, 1.0);
        double u = distr(gen);
        if (u < (c - a) / (b - a)) {
            return a + sqrt(u * (b - a) * (c - a));
        } else {
            return b - sqrt((1 - u) * (b - a) * (b - c));
        }
    }
};

8.3.3 使用示例

std::mt19937 gen(std::random_device{}());
TriangularDistribution distr(0.0, 10.0, 5.0);
for (int i = 0; i < 100; ++i) {
    std::cout << distr(gen) << std::endl;
}

这个示例生成了100个遵循三角形分布的随机数,范围在 [0.0, 10.0],峰值在5.0。

8.4 注意事项

  1. 确保分布的正确性:在实现自定义分布时,必须确保分布的形状和特性与预期相符。可以使用统计方法验证分布的正确性。
  2. 性能考虑:在某些应用中,可能需要高效地生成大量的随机数。在这种情况下,需要确保自定义分布的实现是高效的。
  3. 避免过度复杂:正如心理学家William of Ockham所说:“不应当做无必要的假设。”(不应该做不必要的假设)。在设计自定义分布时,应该尽量简单,避免不必要的复杂性。

9. C++版本与随机数分布

在C++的发展历程中,随机数生成和分布的处理经历了多次的改进和扩展。从C++11开始,标准库引入了一个全新的随机数库,为我们提供了更强大、更灵活的随机数生成和分布工具。

9.1 C++11的革命

C++11是一个里程碑,它引入了头文件,为我们提供了一系列的随机数生成器和分布。在此之前,C++程序员通常依赖于C语言的rand()函数和RAND_MAX宏来生成随机数,但这种方法有其局限性,例如随机数的质量不高,分布不均匀等。

9.1.1 为什么C++11的随机库如此重要?

从心理学的角度来看,人类的大脑善于寻找模式。当我们观察到rand()生成的随机数序列中存在某种模式或规律时,我们可能会对其产生怀疑。C++11的随机库通过提供多种随机数生成器和分布,使得生成的随机数更接近真正的随机性,从而满足了我们的心理预期。

“随机性是自然的,但真正的随机性是难以捉摸的。” - Donald Knuth(计算机科学家,C++名著《计算机程序设计艺术》的作者)

9.1.2 示例与注释

考虑以下使用C++11随机库的示例:

#include <random>
#include <iostream>
int main() {
    std::random_device rd;  // 真随机数生成器(真随机数生成器)
    std::mt19937 gen(rd()); // 使用Mersenne Twister算法的伪随机数生成器(伪随机数生成器)
    std::uniform_int_distribution<> distr(1, 6); // 均匀分布(均匀分布)
    for (int i = 0; i < 10; ++i) {
        std::cout << distr(gen) << " "; // 生成1到6之间的随机数
    }
    return 0;
}

在上述示例中,我们首先使用std::random_device来生成一个真正的随机数,然后使用这个随机数来初始化Mersenne Twister伪随机数生成器。最后,我们使用均匀分布来生成1到6之间的随机数,模拟掷骰子的效果。

9.2 C++14, C++17, C++20的进展

虽然C++11为我们提供了一个强大的随机库,但在后续的C++版本中,这个库并没有经历太大的变化。主要的改进集中在其他领域,如模板元编程、并发和并行编程等。

但是,随着C++的发展,社区对随机库的使用和反馈也在不断增加,这为未来的版本提供了改进的可能性。

9.2.1 底层源码分析

当我们深入到C++随机库的底层实现时,我们可以看到其复杂性和精妙之处。例如,Mersenne Twister算法是如何确保其生成的随机数序列具有很长的周期和良好的统计特性的。

“深入理解原理是掌握技术的关键。” - Bjarne Stroustrup(C++的创始人)

9.2.2 方法对比

方法/特性 C++11 C++14 C++17 C++20
<random>
Mersenne Twister
新增的分布

从上表中,我们可以看到在不同的C++版本中,随机库的核心功能和方法保持不变。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。

目录
相关文章
|
1月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
存储 C++
【C++篇】C++类和对象实践篇——从零带你实现日期类的超详细指南
【C++篇】C++类和对象实践篇——从零带你实现日期类的超详细指南
24 2
【C++篇】C++类和对象实践篇——从零带你实现日期类的超详细指南
|
1月前
|
存储 编译器 C语言
C++类与对象深度解析(一):从抽象到实践的全面入门指南
C++类与对象深度解析(一):从抽象到实践的全面入门指南
48 8
|
1月前
|
C++
C++番外篇——虚拟继承解决数据冗余和二义性的原理
C++番外篇——虚拟继承解决数据冗余和二义性的原理
39 1
|
1月前
|
存储 编译器 程序员
C++类型参数化
【10月更文挑战第1天】在 C++ 中,模板是实现类型参数化的主要工具,用于编写能处理多种数据类型的代码。模板分为函数模板和类模板。函数模板以 `template` 关键字定义,允许使用任意类型参数 `T`,并在调用时自动推导具体类型。类模板则定义泛型类,如动态数组,可在实例化时指定具体类型。模板还支持特化,为特定类型提供定制实现。模板在编译时实例化,需放置在头文件中以确保编译器可见。
32 11
|
2月前
|
C++
c++继承层次结构实践
这篇文章通过多个示例代码,讲解了C++中继承层次结构的实践应用,包括多态、抽象类引用、基类调用派生类函数,以及基类指针引用派生类对象的情况,并提供了相关的参考链接。
|
6天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
33 4
|
8天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
27 4