【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++版本中,随机库的核心功能和方法保持不变。

结语

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

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

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

目录
相关文章
|
4天前
|
存储 负载均衡 算法
基于 C++ 语言的迪杰斯特拉算法在局域网计算机管理中的应用剖析
在局域网计算机管理中,迪杰斯特拉算法用于优化网络路径、分配资源和定位故障节点,确保高效稳定的网络环境。该算法通过计算最短路径,提升数据传输速率与稳定性,实现负载均衡并快速排除故障。C++代码示例展示了其在网络模拟中的应用,为企业信息化建设提供有力支持。
35 15
|
2月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
160 77
|
22天前
|
算法 Serverless 数据处理
从集思录可转债数据探秘:Python与C++实现的移动平均算法应用
本文探讨了如何利用移动平均算法分析集思录提供的可转债数据,帮助投资者把握价格趋势。通过Python和C++两种编程语言实现简单移动平均(SMA),展示了数据处理的具体方法。Python代码借助`pandas`库轻松计算5日SMA,而C++代码则通过高效的数据处理展示了SMA的计算过程。集思录平台提供了详尽且及时的可转债数据,助力投资者结合算法与社区讨论,做出更明智的投资决策。掌握这些工具和技术,有助于在复杂多变的金融市场中挖掘更多价值。
47 12
|
2月前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
72 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
20天前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
102 0
|
2月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
81 19
|
2月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
71 13
|
2月前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
61 12
|
2月前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
60 10
|
22天前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。