C++:58---标准库特殊设施之随机数random(随机数引擎类、随机数分布类)

一、C++随机数库概述

C库函数rand概述

  • 在新标准出现之前,C和C++都依赖于一个简单的C库函数rand来生成随机数
  • 该函数生成均匀分布的伪随机整数,每个随机数的范围在0和一个系统相关的最大值之间(参阅RAND_MAX常量:https://blog.csdn.net/qq_41453285/article/details/103456053
  • rand函数有一些问题:
    • 一些应用需要随机浮点数
    • 一些程序需要非均匀分布的书
    • 如果程序员为了解决上面的问题而试图转换rand生成的随机数的范围、类型或分布时,常常会引入非随机性
  • 定义在头文件random中的随机数库通过一组协作的类来解决这些问题:
    • 随机数引擎类:一个引擎类可以生成unsigned随机数序列
    • 随机数分布类:一个分布类使用一个引擎类生成指定类型的、在给定范围内的、服从特定概率分布的随机数

随机数引擎类

  • 标准库定义了三个类,实现不同的算法来生成随机数
  • 标准库还定义了三个适配器,可以修改给定引擎生成的序列
  • 引擎和引擎适配器类都是模板。与随机数分不累的参数不同,这些引擎的参数更为复杂,且需要深入了解特定引擎使用的数学知识
  • 标准库还定义了几个从引擎和适配器类型构造的类型。default_random_engine类型是一个参数化的引擎类型的类型别名,参数化所用的变量的目的是在通常情况下获得好的性能。标准库还定义了几个类,它们都是一个引擎或适配器的完全特例化版本。定义如下:

随机数分布类

  • 除了总是生成bool类型的bernoulli_distribution外,其他分布类都是模板。每个模板都接受单个类型参数,它指出了分布生成的结果类型

  • 分布类与我们已经使用过的其它模板不同,它们限制了我们可以为模板类型指定哪些类型:

    • 一些分布模板只能用来生成浮点数

    • 而其他模板只能用来生成证书

  • 下面的描述中:
    • 通过将类型说明为template_name<RealT>来指出分布生成浮点数。对这些模板,我们可以使用float、double或long double替代RealT
    • 类似的,IntT表示要求一个内置整数类型,但不包括bool类型或任何char类型。我们可以使用short、int、long、long long、unsigned short、unsigned int、unsigned long、unsigned long long来代替IntT
  • 分布模板定义了一个默认模板类型参数。整型分布的默认参数是int,生成浮点数的模板的默认实参是double
  • 每个分布的构造函数都有这种特定的参数。某些参数指出了分布的范围。这些范围与迭代器范围不同。都是包含的

二、引擎类和分布类的基本使用

  • 在应用中,我们一般不直接使用随机数引擎类的输出,因为这些都是原始随机数,可能与我们的需求不符。当我们使用随机数引擎类生成随机数之后,一般配合随机数分布类来生成指定范围内的数

随机数引擎类

  • 随机数引擎类是函数对象类,定义了一个调用运算符(),该运算符不接受参数并返回一个随机unsigned整数
  • 我们可以通过调用一个随机数引擎类对象来生成原始随机数:
#include <iostream>
#include <random>
using namespace std;

int main()
{
    std::default_random_engine e;     //生成随机无符号数
    for (size_t i = 0; i < 10; ++i)   //使用调用运算符(),来生成一个随机数
        std::cout << e() << std::endl;
    return 0;
}

  • 标准库定义了很多的随机数引擎类(见上),区别在于性能和随机性质量不同。每个编译器都会指定其中一个作为default_random_engine类型。此类型一般具有最常用的特性
  • 下图给出了随机数引擎类的操作:

随机数分布类

  • 为了得到一个指定范围内的数,我们先使用随机数引擎类生成随机数,然后再使用随机数分布类来生成一个分布类型的对象
  • 分布类也是函数对象。分布类定义了一个调用运算符(),接受一个随机数引擎作为参数。分布对象使用它的引擎参数生成随机数,并将其映射到指定的分布
  • 例如:
    • 使用uniform_int_distribution生成均匀分布的unsigned值,并且提供了期望得到的最大值(9)和最小值(0)
    • 然后将引擎类传递给分布类
#include <iostream>
#include <string>
#include <random>
using namespace std;

int main()
{
    std::uniform_int_distribution<unsigned> u(0, 9); //随机数分布类
    std::default_random_engine e;                    //随机数引擎类
    for (size_t i = 0; i < 10; ++i)
        std::cout << u(e) << " ";                    //分布类调用引擎类,来生成指定范围内的数
    std::cout << std::endl;
    return 0;
}

  • 注意:我们传递给分布对象的是引擎对象本身,而不是引擎对象生成的值。例如:
std::uniform_int_distribution<unsigned> u(0, 9); //分布类
std::default_random_engine e;                    //引擎类

u(e);   //错误的
u(e()); //正确的
  • 下面是随机数分布类的支持的操作:

 

三、随机数引擎类生成数的范围

  • 在上面我们可以看出随机数引擎类default_random_engine对象的输出与rand函数类似
  • 不同之处为:
    • rand函数生成的数的范围在0到RAND_MAX之间
    • 而随机数引擎类生成的unsigned整数在一个系统定义的范围内。一个引擎类型的范围可以通过调用该对象的min和max成员来获得
  • 例如:
std::default_random_engine e;
std::cout << "min: " << e.min() << '\n' << "max: " << e.max() << std::endl;

四、引擎类生成的数值序列(重要)

  • 随机数有一个特性经常会使新手迷惑:即使生成的数看起来是随机的,但对一个给定的发生器,每次运行程序都会返回相同的数值序列
  • 序列不变这一事实在调试时非常有用。在另一方面,使用随机数发生器的程序页必须考虑这一点

演示案例

  • 例如下面编写一个函数,生成一个vector,包含100个均匀分布在0~9之间的随机数:
//这个函数虽然语法没有错误,但是每次运行都返回相同的随机数
std::vector<unsigned> bad_randVec()
{
    std::default_random_engine e;
    std::uniform_int_distribution<unsigned> u(0, 9);

    std::vector<unsigned> ret;
    for (size_t i = 0; i < 100; ++i)
        ret.push_back(u(e));

    return ret;
}
  • 上面的函数每次调用都会生成相同的随机数,即使程序关闭重新运行也是如此。因此,每回返回的vector中的元素都是相同的
  • 我们编写一段代码来验证一下:
int main()
{
    std::vector<unsigned> v1(bad_randVec());
    std::vector<unsigned> v2(bad_randVec());
	
    std::cout << ((v1 == v2) ? "equal" : "not equal") << std::endl;
    return 0;
}

将引擎和关联的分布对象定义为static

  • 如果想要每次返回的随机数都不相同,应该将随机数引擎类和随机数分布类定义为static
  • 为什么定义为static就可以了?
    • 因为变量定义为static,第一次定义之后就会永久存在于程序中,不会随着函数的执行结束而销毁
    • 第一次执行随机数分布类时,生成100个随机数。当下一次再执行到相同的随机数分布类时,会获得接下来的100个随机数,以此类推......
#include <iostream>
#include <vector>
#include <random>
using namespace std;

std::vector<unsigned> bad_randVec()
{
    static std::default_random_engine e;
    static std::uniform_int_distribution<unsigned> u(0, 9);

    std::vector<unsigned> ret;
    for (size_t i = 0; i < 100; ++i)
        ret.push_back(u(e));

    return ret;
}

int main()
{
    std::vector<unsigned> v1(bad_randVec());
    std::vector<unsigned> v2(bad_randVec());
	
    std::cout << ((v1 == v2) ? "equal" : "not equal") << std::endl;
    return 0;
}

五、设置随机数发生器种子

  • 上面我们介绍了,对于同一个随机数分布引擎和分布类,每次随机数生成的序列都是相同的
  • 但是我们希望每次运行程序会生成不同的随机结果,可以通过提供一个种子来达到这一目的。种子就是一个数值,引擎可以利用它从序列中一个新位置重新开始生成随机数
  • 为引擎设置种子有两种方法:
    • 在创建对象时提供种子
    • 调用引擎的seed函数来设置

演示案例

  • 下面定义了四个引擎:
    • 前两个引擎的种子不同,因此生成不同的序列
    • 后两个引擎的种子相同,因此生成相同的序列
int main()
{
    std::default_random_engine e1;
    std::default_random_engine e2(2177483646);
    std::default_random_engine e3;
    e3.seed(32767);
    std::default_random_engine e4(32767);

    for (size_t i = 0; i != 100; ++i) {
        if (e1() == e2()) //false
            std::cout << "unseeded match at iteration: " << i << std::endl;
        if (e3() != e4()) //false
            std::cout << "seeded differs at iteration: " << i << std::endl;
    }
    return 0;
}
  • 因此上面的程序运行结果无任何显示

time函数

  • 选择一个好的种子,与生成好的随机数所涉及的其它大多数事情相同,是极其困难的。可能最常用的方法是调用系统函数time
  • 这个函数定义在头文件ctime中,它返回从一个特定时刻到当前经过了多少秒
  • time函数接受单个指针参数,指向用于写入时间的数据结构。如果次指针为空,则函数简单地返回时间
  • 例如:
#include <iostream>
#include <vector>
#include <random>
#include <ctime>
using namespace std;

int main()
{
    std::default_random_engine e1(std::time(0));
    for (size_t i = 0; i < 5;++i)
        std::cout << e1() << std::endl;
    return 0;
}

  • 由于time返回以秒计的时间,因此这种方式适用于生成种子的间隔为秒级或更长的引用

六、随机数分布类再探

  • 随机数引擎生成的unsigned数,范围内的每个数被生成的概率都是相同的。而应用程序常常需要不同类型或不同分布的随机数。标准库通过定义不同随机数分布类来满足这两方面的要求
  • 分布类对象和引擎类对象协同工作,生成要求的结果

生成随机实数(uniform_real_distribution类)

  • 如果使用rand()函数,那么:
    • 如果使用rand()函数生成浮点数,那么方法是使用rand()的结果除以RAND_MAX,即,系统定义的rand可以生成的最大随机数的上界
    • 这种方法不正确的原因是随机整数的精度通常低于随机浮点数,这样,有一些浮点值永远都不会被生成
  • uniform_real_distribution分布类用来处理生成浮点数
  • 先使用随机数引擎类提供随机整数,然后传递给uniform_real_distribution生成浮点数
  • 例如,下面的代码用来生成0到1之间的浮点数
#include <iostream>
#include <random>
using namespace std;

int main()
{
    std::default_random_engine e; //生成无符号整数
    //生成0到1之间的浮点数(均匀分布)
    std::uniform_real_distribution<double> u(0, 1);

    //依次获取10个浮点数
    for (size_t i = 0; i < 10; ++i)
        std::cout << u(e) << " ";
    std::cout << std::endl;
    return 0;
}

使用分布类的默认结果类型

  • 分布类都是模板,具有单一的模板类型参数,表示分布生成的随机数的数据类型。(对此有一个例外,将在下面的bernoulli_distribution)
  • 每个分部模板都有一个默认模板实参。例如:
    • 生成浮点数的随机数分布类生成double值
    • 生成整型值的随机数分布类生成int值
  • 例如:
//默认生成浮点数,因此我们可以省略模板实参类型
std::uniform_real_distribution<> u(0, 1);

生成非均匀分部的随机数

  • 除了正确生成在指定范围内的数之外,新标准库的另一个优势是可以生成非均匀分布的随机数。实际上,标准库定义了20种分布类型(见文章最开始)
  • 演示案例:
    • 我们生成一个正态分布的序列,并画出值的分布
    • 由于normal_distribution生成浮点数,我们的程序使用lround函数将每个随机数舍入到最接近的整数
    • 我们将生成200个数,它们以4为中心,标准差为1.5
    • 由于使用正态分布,我们期望生成的数中大约99%都在0到8之间(包含)
#include <iostream>
#include <string>
#include <vector>
#include <random>
#include <cmath> //lround
using namespace std;

int main()
{
    std::default_random_engine e;         //生成随机整数
    std::normal_distribution<> n(4, 1.5); //均值为4,标准差为1.5
    std::vector<unsigned> vals(9);        //9个元素均为0

    for (size_t i = 0; i != 200; ++i) 
    {
        unsigned v = std::lround(n(e));   //舍入到最接近的整数
        if (v < vals.size())              //如果结果在范围内
            ++vals[v];                    //统计每个数出现了多少次
    }

    //循环打印
    for (size_t j = 0; j != vals.size(); ++j)
        std::cout << j << ": " << std::string(vals[j], '*') << std::endl;
    return 0;
}
  • 程序运行结果如下,可以看到不是明显的均匀分布

bernoulli_distribution类

  • 我们在上面说过随机数分布类是一个模板,但是bernoulli_distribution是一个例外,其是一个类而不是模板
  • 此分布总是返回一个bool值,它返回true的概率是一个常数,此概率的默认值为0.5
  • 演示案例:
    • 编写一个程序,这个程序与用户进行互动
    • 其中一个游戏者——用户或者程序——必须先执行
    • 我们可以用一个值范围是0到1的uniform_int_distribution来选择先行的游戏者,但也可以用伯努利分布来完成这个选择
    • 假定已有一个名为play的函数进行游戏
    • 使用一个do while循环来反复提示用户进行游戏
#include <iostream>
#include <string>
#include <random>
using namespace std;

bool play(bool value)
{
    //..进行游戏	
}

int main()
{
    std::string resp; //输入y代表继续游戏,输入其他退出游戏
    std::default_random_engine e;  //e应该保持状态,因此必须定义在循环外面
    std::bernoulli_distribution b;

    do
    {
        bool first = b(e); //随机返回true或false
        //传递谁先进行游戏的指示
        std::cout << (first ? "We go first" : "You get to go first") << std::endl;
        std::cout << ((play(first)) ? "Sorry you lost" : "congrats,you won") << std::endl;
    } while (std::cin >> resp&&resp[0] == 'y');

    return 0;
}

  • 使用bernoulli_distribution的另一个好处是,我们可以先行调整概述。例如下面给程序一个较小的优势:
std::bernoulli_distribution b(.55);
  • 如果使用上面的定义,则程序有55/45的机会先行
发布了1504 篇原创文章 · 获赞 1063 · 访问量 43万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104675186