MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 随机数函数用法详解

2023-01-084.5k 阅读

C++ 中的随机数生成简介

在 C++ 编程中,生成随机数是一项常见的任务,它在游戏开发、模拟、密码学等众多领域都有广泛应用。C++ 提供了多种生成随机数的方式,早期主要依赖于标准库中的 <stdlib.h> 头文件相关函数,后来随着 C++11 的发布,引入了更为强大和灵活的随机数生成库 <random>

<stdlib.h> 中的随机数函数

  1. rand() 函数
    • 基本用法rand() 函数是 C 和 C++ 标准库中用于生成伪随机数的函数。它定义在 <stdlib.h> 头文件中。rand() 函数会返回一个介于 0(包括)和 RAND_MAX(包括)之间的整数。RAND_MAX<stdlib.h> 中定义的一个常量,其值至少为 32767。
    • 代码示例
#include <iostream>
#include <stdlib.h>

int main() {
    for (int i = 0; i < 5; ++i) {
        int randomNumber = rand();
        std::cout << "Random number: " << randomNumber << std::endl;
    }
    return 0;
}
  • 本质原理rand() 函数生成的是伪随机数,它基于一个内部的状态机。每次调用 rand() 时,它会根据前一个状态计算出下一个伪随机数。如果程序不设置随机数种子,每次运行程序时,rand() 函数会生成相同的随机数序列,因为它的初始状态是固定的。
  1. srand() 函数
    • 基本用法srand() 函数用于设置 rand() 函数的随机数种子。通过设置不同的种子值,可以让 rand() 生成不同的随机数序列。srand() 接受一个 unsigned int 类型的参数作为种子。
    • 代码示例
#include <iostream>
#include <stdlib.h>
#include <time.h>

int main() {
    // 使用当前时间作为种子
    srand(static_cast<unsigned int>(time(nullptr)));
    for (int i = 0; i < 5; ++i) {
        int randomNumber = rand();
        std::cout << "Random number: " << randomNumber << std::endl;
    }
    return 0;
}
  • 本质原理:在上述代码中,time(nullptr) 获取当前的系统时间,其返回值类型为 time_t,将其转换为 unsigned int 类型作为 srand() 的参数。由于每次程序运行时的系统时间不同,这样就可以保证每次运行程序时 rand() 生成的随机数序列不同。然而,这种基于时间的种子设置方式并非在所有场景下都适用,例如在需要可重复性的模拟场景中,可能需要手动设置固定的种子值。
  1. 生成指定范围内的随机数
    • 基本方法:要生成指定范围内的随机数,可以通过对 rand() 的返回值进行数学运算。假设要生成一个介于 minmax(包括 minmax)之间的整数 randomNumber,可以使用以下公式:randomNumber = min + (rand() % (max - min + 1))
    • 代码示例
#include <iostream>
#include <stdlib.h>
#include <time.h>

int main() {
    srand(static_cast<unsigned int>(time(nullptr)));
    int min = 10;
    int max = 20;
    for (int i = 0; i < 5; ++i) {
        int randomNumber = min + (rand() % (max - min + 1));
        std::cout << "Random number in range [" << min << ", " << max << "]: " << randomNumber << std::endl;
    }
    return 0;
}
  • 本质原理rand() % (max - min + 1) 这部分运算会生成一个介于 0 到 max - min 之间的整数,再加上 min 就可以将其范围调整到 [min, max]。不过需要注意的是,这种方法生成的随机数分布可能并不均匀,尤其是当 RAND_MAX 与要生成的范围相比不是足够大时,可能会出现某些值出现的概率略高于其他值的情况。

C++11 的 <random>

  1. <random> 库的优势
    • 更好的随机数分布<random> 库提供了多种随机数分布引擎和分布类型,能够生成更均匀、更符合各种统计分布的随机数。例如,可以生成正态分布、泊松分布等各种分布的随机数,而 <stdlib.h> 中的函数主要生成均匀分布的整数随机数。
    • 可重复性和灵活性<random> 库允许更精确地控制随机数生成过程,包括设置种子、选择不同的随机数引擎等,这使得在需要可重复性的场景(如模拟测试)中更容易实现。同时,它的面向对象设计使得代码结构更清晰,更易于维护和扩展。
  2. 随机数引擎
    • 常见引擎类型
      • std::default_random_engine:这是一个默认的随机数引擎,它根据实现定义选择一个合适的引擎。在不同的编译器实现中,它可能基于不同的底层算法。例如,在某些实现中,它可能基于 Mersenne Twister 算法。
      • std::mt19937:这是一种广泛使用的随机数引擎,基于 Mersenne Twister 算法。它具有周期长(2^19937 - 1)、均匀性好等优点,适用于各种需要高质量随机数的场景,如蒙特卡罗模拟等。
      • std::linear_congruential_engine:这是一种基于线性同余法的随机数引擎。其公式为 ( X_{n+1}=(aX_n + c)\bmod m ),其中 ( a )、( c ) 和 ( m ) 是常量,( X_n ) 是当前的随机数,( X_{n + 1} ) 是下一个随机数。这种引擎相对简单,计算速度较快,但随机数质量可能不如 std::mt19937
    • 代码示例
#include <iostream>
#include <random>

int main() {
    // 使用 std::mt19937 引擎
    std::random_device rd;
    std::mt19937 gen(rd());
    for (int i = 0; i < 5; ++i) {
        int randomNumber = gen();
        std::cout << "Random number from mt19937: " << randomNumber << std::endl;
    }
    return 0;
}
  • 本质原理:在上述代码中,std::random_device rd 用于获取一个真正的随机数种子(通常基于硬件,如读取设备驱动的噪声等)。std::mt19937 gen(rd()) 使用这个种子初始化 std::mt19937 引擎。每次调用 gen() 时,引擎会根据 Mersenne Twister 算法的内部状态更新并返回一个新的伪随机数。
  1. 随机数分布
    • 均匀分布
      • 整数均匀分布std::uniform_int_distribution 用于生成指定范围内的均匀分布的整数随机数。其构造函数接受两个参数,分别是分布范围的下限(包括)和上限(包括)。
      • 代码示例
#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> distrib(10, 20);
    for (int i = 0; i < 5; ++i) {
        int randomNumber = distrib(gen);
        std::cout << "Random number in range [10, 20]: " << randomNumber << std::endl;
    }
    return 0;
}
 - **本质原理**:`std::uniform_int_distribution<> distrib(10, 20)` 创建了一个分布对象,该对象会根据 `gen` 提供的随机数生成源,生成介于 10 到 20 之间(包括 10 和 20)的均匀分布的整数随机数。`distrib(gen)` 调用分布对象的函数调用运算符,从 `gen` 中获取随机数,并根据分布规则进行转换,返回符合指定范围的随机数。
  • 实数均匀分布std::uniform_real_distribution 用于生成指定范围内的均匀分布的实数随机数。其构造函数同样接受下限和上限参数。
  • 代码示例
#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> distrib(10.0, 20.0);
    for (int i = 0; i < 5; ++i) {
        double randomNumber = distrib(gen);
        std::cout << "Random real number in range [10.0, 20.0]: " << randomNumber << std::endl;
    }
    return 0;
}
  • 正态分布
    • 基本用法std::normal_distribution 用于生成符合正态分布(高斯分布)的随机数。其构造函数接受两个参数,分别是均值((\mu))和标准差((\sigma))。
    • 代码示例
#include <iostream>
#include <random>
#include <cmath>
#include <iomanip>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::normal_distribution<> distrib(50.0, 10.0);
    for (int i = 0; i < 10; ++i) {
        double randomNumber = distrib(gen);
        std::cout << std::fixed << std::setprecision(2) << "Random number from normal distribution (mean=50, stddev=10): " << randomNumber << std::endl;
    }
    return 0;
}
 - **本质原理**:正态分布的概率密度函数为 \( f(x)=\frac{1}{\sigma\sqrt{2\pi}}e^{-\frac{(x - \mu)^2}{2\sigma^2}} \),其中 \(\mu\) 是均值,\(\sigma\) 是标准差。`std::normal_distribution` 对象根据传入的均值和标准差,利用 `gen` 提供的随机数生成源,按照正态分布的概率密度函数生成随机数。生成的随机数在均值附近出现的概率较高,随着与均值距离的增加,出现的概率逐渐降低。
  • 泊松分布
    • 基本用法std::poisson_distribution 用于生成符合泊松分布的随机数。其构造函数接受一个参数,即泊松分布的 lambda 值((\lambda)),表示单位时间(或单位面积)内随机事件的平均发生次数。
    • 代码示例
#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::poisson_distribution<> distrib(3.0);
    for (int i = 0; i < 10; ++i) {
        int randomNumber = distrib(gen);
        std::cout << "Random number from Poisson distribution (lambda=3): " << randomNumber << std::endl;
    }
    return 0;
}
 - **本质原理**:泊松分布用于描述在一定时间或空间内,某事件发生的次数的概率分布。其概率质量函数为 \( P(X = k)=\frac{e^{-\lambda}\lambda^k}{k!} \),其中 \( k \) 是事件发生的次数,\(\lambda\) 是平均发生次数。`std::poisson_distribution` 对象根据传入的 \(\lambda\) 值,利用 `gen` 提供的随机数生成源,按照泊松分布的概率质量函数生成随机数。生成的随机数表示在给定平均发生次数 \(\lambda\) 的情况下,事件发生 \( k \) 次的随机结果。

4. 设置种子

  • 使用 std::seed_seqstd::seed_seq<random> 库中用于生成种子序列的类。它可以接受多个整数作为输入,并通过内部算法生成一个种子序列,用于初始化随机数引擎。这种方式比简单地使用单个整数作为种子更能提高随机数生成的质量和可重复性。
  • 代码示例
#include <iostream>
#include <random>
#include <vector>

int main() {
    std::vector<int> seeds = {1, 2, 3, 4, 5};
    std::seed_seq seq(seeds.begin(), seeds.end());
    std::mt19937 gen(seq);
    for (int i = 0; i < 5; ++i) {
        int randomNumber = gen();
        std::cout << "Random number with seed sequence: " << randomNumber << std::endl;
    }
    return 0;
}
  • 本质原理:在上述代码中,std::vector<int> seeds 定义了一个包含多个整数的向量。std::seed_seq seq(seeds.begin(), seeds.end()) 使用这些整数初始化 std::seed_seq 对象。std::seed_seq 内部通过特定的算法将这些输入整数混合,生成一个更复杂的种子序列。std::mt19937 gen(seq) 使用这个种子序列初始化 std::mt19937 随机数引擎,从而使得每次运行程序时,如果种子向量 seeds 不变,生成的随机数序列将是相同的,提高了可重复性。同时,由于种子序列的复杂性,也增强了随机数生成的随机性。

随机数生成的应用场景

  1. 游戏开发
    • 角色属性生成:在角色扮演游戏中,角色的属性(如力量、敏捷、智力等)通常需要通过随机数来生成。例如,可以使用 <random> 库中的均匀分布来生成一定范围内的属性值。假设角色的力量属性范围是 1 到 100,可以这样实现:
#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> distrib(1, 100);
    int strength = distrib(gen);
    std::cout << "Character strength: " << strength << std::endl;
    return 0;
}
  • 随机事件触发:游戏中的随机事件(如遇到怪物、发现宝藏等)可以通过生成随机数来决定是否触发。可以设置一个概率,例如有 20% 的概率触发某个事件,通过生成 0 到 99 之间的随机数,如果随机数小于 20,则触发事件。
#include <iostream>
#include <random>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> distrib(0, 99);
    int randomValue = distrib(gen);
    if (randomValue < 20) {
        std::cout << "Random event triggered!" << std::endl;
    } else {
        std::cout << "No random event." << std::endl;
    }
    return 0;
}
  1. 模拟与仿真
    • 蒙特卡罗模拟:蒙特卡罗模拟是一种通过随机抽样来解决数学、物理和工程问题的方法。例如,计算圆周率 (\pi)。可以在一个边长为 1 的正方形内随机生成大量的点,统计落在以正方形一个顶点为圆心、半径为 1 的四分之一圆内的点的数量。根据圆与正方形的面积比关系,可以近似计算出 (\pi) 的值。
#include <iostream>
#include <random>
#include <cmath>

int main() {
    const int numPoints = 1000000;
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_real_distribution<> distrib(0.0, 1.0);
    int insideCircle = 0;
    for (int i = 0; i < numPoints; ++i) {
        double x = distrib(gen);
        double y = distrib(gen);
        if (std::sqrt(x * x + y * y) <= 1.0) {
            ++insideCircle;
        }
    }
    double piEstimate = 4.0 * static_cast<double>(insideCircle) / numPoints;
    std::cout << "Estimated value of pi: " << piEstimate << std::endl;
    return 0;
}
  • 排队系统模拟:在模拟排队系统时,需要生成顾客到达时间间隔和服务时间等随机变量。可以使用指数分布来模拟顾客到达时间间隔(因为在实际情况中,顾客到达时间间隔往往符合指数分布),使用正态分布来模拟服务时间(假设服务时间围绕某个平均值波动)。
#include <iostream>
#include <random>
#include <vector>

int main() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::exponential_distribution<> arrivalDistrib(0.5);
    std::normal_distribution<> serviceDistrib(5.0, 1.0);
    std::vector<double> arrivalTimes;
    std::vector<double> serviceTimes;
    double currentTime = 0.0;
    for (int i = 0; i < 10; ++i) {
        double arrivalInterval = arrivalDistrib(gen);
        currentTime += arrivalInterval;
        arrivalTimes.push_back(currentTime);
        double serviceTime = serviceDistrib(gen);
        serviceTimes.push_back(serviceTime);
    }
    for (int i = 0; i < 10; ++i) {
        std::cout << "Customer " << i + 1 << " arrives at time: " << arrivalTimes[i] << ", service time: " << serviceTimes[i] << std::endl;
    }
    return 0;
}
  1. 密码学
    • 密钥生成:在密码学中,需要生成高质量的随机密钥。<random> 库中的 std::random_device 可以用于获取真正的随机数,这些随机数可用于生成密钥。例如,生成一个 128 位的随机密钥:
#include <iostream>
#include <random>
#include <iomanip>
#include <sstream>

std::string generateKey() {
    std::random_device rd;
    std::array<char, 16> keyData;
    for (auto& byte : keyData) {
        byte = static_cast<char>(rd());
    }
    std::ostringstream oss;
    oss << std::hex << std::setfill('0');
    for (auto byte : keyData) {
        oss << std::setw(2) << static_cast<unsigned>(static_cast<unsigned char>(byte));
    }
    return oss.str();
}

int main() {
    std::string key = generateKey();
    std::cout << "Generated key: " << key << std::endl;
    return 0;
}
  • 随机化加密参数:在加密算法中,有时需要随机化一些参数,如初始化向量(IV)。可以使用 <random> 库生成符合特定要求的随机值作为 IV,增加加密的安全性。

随机数生成的注意事项

  1. 随机数的质量
    • 避免使用低质量的随机数生成器:在某些情况下,如密码学应用中,使用 <stdlib.h> 中的 rand() 函数可能会带来安全风险,因为它生成的伪随机数质量相对较低,容易被预测。应优先使用 <random> 库中的高质量随机数引擎和分布。
    • 评估随机数分布:在使用随机数生成器时,要根据具体需求评估随机数的分布是否符合要求。例如,在模拟排队系统时,如果错误地使用了均匀分布而不是指数分布来模拟顾客到达时间间隔,可能会导致模拟结果与实际情况相差较大。
  2. 可重复性
    • 测试和调试:在测试和调试程序时,有时需要程序能够生成相同的随机数序列,以便于重现问题。可以通过设置固定的种子值来实现。例如,在使用 <random> 库时,可以手动设置 std::seed_seq 的输入,或者使用固定的种子值初始化随机数引擎。
    • 生产环境:在生产环境中,通常需要随机数具有不可预测性,因此要避免使用固定的种子值。可以使用 std::random_device 来获取真正的随机数种子,除非有特殊的可重复性需求。
  3. 性能考虑
    • 不同引擎和分布的性能差异:不同的随机数引擎和分布在性能上可能存在差异。例如,std::linear_congruential_engine 计算速度较快,但随机数质量相对较低;std::mt19937 随机数质量高,但计算量可能相对较大。在选择随机数生成方式时,要根据应用场景的性能要求进行权衡。
    • 缓存和预生成:在某些性能敏感的应用中,可以考虑缓存预先生成的随机数,以减少每次生成随机数的开销。例如,在游戏开发中,如果需要频繁生成随机数,可以一次性生成一批随机数并缓存起来,需要时直接从缓存中获取。

通过深入了解 C++ 中不同的随机数生成方式及其本质原理、应用场景和注意事项,开发者能够根据具体需求选择最合适的随机数生成方法,从而编写出更高效、更安全、更符合实际需求的程序。无论是在游戏开发、模拟仿真还是密码学等领域,合理使用随机数生成技术都能为程序带来更多的可能性和功能。