C++ 洗牌函数std::shuffle的用法

目录

1.简介

2.工作原理

3.std::shuffle 与 std::random_shuffle 的区别

4.rand 和 srand

5.std::shuffle 的使用方法

6.随机数生成器和分布器

7.注意事项


1.简介

        std::shuffle 是 C++ 标准库中用于对序列进行随机重排(洗牌)的一种算法。它可以将容器(例如 std::vectorstd::array、或普通数组等)中的元素随机地打乱顺序,就像洗扑克牌一样。与早期的 std::random_shuffle 相比,std::shuffle 要求使用一个随机数引擎(如 std::default_random_engine),从而在提供更可控、更安全的随机生成方式的同时,也避免了潜在的随机质量问题。这个算法定义在 <algorithm> 头文件中,并且需要 C++11 或更高版本的支持。

        函数原型:

template< class RandomIt, class URNG >
void shuffle( RandomIt first, RandomIt last, URNG&& g );

template< class RandomIt >
void shuffle( RandomIt first, RandomIt last );
  • RandomIt:可随机访问的迭代器类型(Random Access Iterator),例如指向 std::vectorstd::array 或内置数组的指针等。

  • URBG:Uniform Random Bit Generator,即随机数引擎类型。比如常用的 std::default_random_enginestd::mt19937(梅森旋转算法引擎)等。

2.工作原理

  std::shuffle 通过多次交换容器中的元素来打乱它们的顺序。每次交换都是基于随机数生成器 g 产生的随机数来决定的。如果未提供随机数生成器,则使用 std::random_device 和 std::default_random_engine 的组合来生成随机数。

3.std::shuffle 与 std::random_shuffle 的区别

        在 C++11 之前,我们常使用 std::random_shuffle 来对容器进行随机洗牌。但从 C++14 开始,std::random_shuffle 被标记为弃用,并在 C++17 中被移除。原因如下:

        随机数来源

  • std::random_shuffle 的默认实现通常基于 rand() 函数来完成随机元素的选择,依赖全局状态;这个函数的随机性质量不够理想,且在不同平台和编译器间不一致。

  • std::shuffle 需要显式传入一个随机数引擎(如 std::default_random_engine),这样在不同机器和编译器上,算法的随机行为更加可控并且符合现代 C++ 中的随机数生成器框架。

        可维护性和安全性

  • std::shuffle 的接口设计更符合现代 C++ 的习惯,使得代码可读性、可维护性更高,同时也避免了因随机数生成方式不一致而产生的可移植性问题。

4.rand 和 srand

这两个是C标准函数,在C++中被放在头文件 <cstdlib> 之中,搜索到的函数声明如下:

__BEGIN_NAMESPACE_STD
/* Return a random integer between 0 and RAND_MAX inclusive.  */
extern int rand (void) __THROW;
/* Seed the random number generator with the given number.  */
extern void srand (unsigned int __seed) __THROW;
__END_NAMESPACE_STD

        其中 std::rand() 是用于返回一个介于[0, RAND_MAX] 范围的伪随机整型值,RAND_MAX 的值最小为 32767,也就是有符号short的最大值,我查到的版本库中的值是2147483647,即有符号int的最大值。

        std::srand() 的作用是为 std::rand() 这个伪随机数生成器设置种子,如果在调用 std::srand() 之前使用了 std::rand(),种子默认为1,相当于调用了 std::srand(1),rand通常不是线程安全的函数,依赖于具体的实现。

        更需要注意的是, std::rand() 生成的是一个伪随机序列,如果随机种子相同,则得到的序列也是相同的,这也是 std::rand 不建议使用的原因,建议是使用C++11随机数生成工具来替换它。

        伪随机序列也并不是“一无是处”,两个进程可以通过设置相同的随机数种子来产生相同的序列,比如可以用于服务器和客户端做帧同步时产生随机数,这样的随机数产生是同步可控的。

        下面举个 std::rand() 使用的例子

#include <iostream>
#include <cstdlib>

int main()
{
    std::srand(1);
    std::cout << std::rand() << std::endl;

    std::srand(1);
    std::cout << std::rand() << std::endl;

    std::srand(1);
    std::cout << std::rand() << std::endl;

    return 0;
}

运行结果如下:

1867856543
1867856543
1867856543

我们可以看到因为随机种子相同,生成的随机数都是同一个,为了使的生成的序列更随机,通常使用当前时间戳 std::time(nullptr) 作为随机种子,然后再生成随机序列:

#include <iostream>
#include <cstdlib>
#include <ctime>

int main()
{
    std::srand(std::time(nullptr));
    std::cout << std::rand() << std::endl;

    std::srand(std::time(nullptr));
    std::cout << std::rand() << std::endl;

    std::srand(std::time(nullptr));
    std::cout << std::rand() << std::endl;

    return 0;
}

运行结果如下:

1864343359
1864343359
1864343359

怎么还是相同的呢?那是因为 std::time(nullptr) 函数返回的时间戳单位是秒,在一秒中内的时间种子是相同的,所以返回的序列也是相同的,通常的使用方法是在程序启动时设置一次时间种子就可以了,并不需要每次都进行设置,而 random_shuffle 中使用了 std::rand() 函数,如果不手动设置时间种子,每次同一时间洗同一副牌,得到的结果也是相同的,所以这也是random_shuffle被后续版本移除的一个原因。

5.std::shuffle 的使用方法

#include <iostream>
#include <vector>
#include <algorithm>
#include <random>   // std::default_random_engine, std::random_device

int main() {
    // 准备数据
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9 , 10, 11, 12};

    // 1) 创建一个随机数引擎,通常以随机种子初始化
    std::random_device rd;  // 硬件随机数生成器(若可用)
    std::default_random_engine rng(rd()); // 使用 rd 产生种子来初始化引擎

    // 2) 使用 std::shuffle 对 [v.begin(), v.end()) 范围内的元素进行随机重排
    std::shuffle(v.begin(), v.end(), rng);

    // 3) 输出打乱后的结果
    std::cout << "Shuffled result: ";
    for (auto i : v) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个示例中,我们使用 std::random_device 获取一个尽量随机的种子,然后用它来初始化一个 std::default_random_engine 引擎。接着将 rng 传递给 std::shuffle 以获得随机化的打乱顺序。

6.随机数生成器和分布器

        random是C++11提供的一个头文件,其中包含多个随机数生成工具,可以使用生成器和分布器的组合产生随机数,其中包含随机数生成器和分布器的多个类实现,分为以下两种:

        Uniform random bit generators (URBGs):均匀随机位生成器,也就是生成均匀分布随机数的对象,可以生成伪随机序列,也可生成真正的随机数序列
        Random number distributions:随机数分布器,用于将URBGs产生的随机数转换为某种特定数学概率分布的序列,如均匀分布、正态分布、泊松分布等

        常见的生成器:

  • linear_congruential_engine: 线性同余生成算法,是最常用也是速度最快的,随机效果一般
  • mersenne_twister_engine: 梅森旋转算法,随机效果最好
  • subtract_with_carry_engine: 滞后Fibonacci算法

常见的适配器,我理解的它的作用是生成器的二次加工厂,对生成器结果进行特定操作

  • discard_block_engine: 丢弃一些数
  • independent_bits_engine: 将序列打包成指定位数的块
  • shuffle_order_engine: 调整序列顺序

预定义的随机数生成器,利用通用生成器和适配器组合出的流行特定生成器:

  • minstd_rand
  • minstd_rand0
  • mt19937: mt是因为这个伪随机数产生器基于Mersenne Twister算法,19937来源于产生随的机数的周期长可达到2^19937-1
  • mt19937_64
  • ranlux24_base
  • ranlux48_base
  • ranlux24
  • ranlux48
  • knuth_b
  • default_random_engine: 编译器可以自行实现

        以上随机数引擎需要一个整型参数作为种子,对于给定的随机数种子,伪随机数生成器总会生成相同的序列,这在测试的时候是相当有用的。而在实际使用时,需要设置随机树作为种子来产出不同的随机数,推荐使用 std::random_device 的值作为随机数种子。

        std::random_device 是一个使用硬件熵源的非确定性随机数发生器,不可预测。

        常见的分布器:

  • uniform_int_distribution: 均匀离散分布
  • uniform_real_distribution: 均匀实数分布
  • bernoulli_distribution: 伯努利分布
  • binomial_distribution: 二项式分布
  • geometric_distribution: 几何分布
  • negative_binomial_distribution: 负二项式分布
  • poisson_distribution: 泊松分布
  • exponential_distribution: 指数分布
  • gamma_distribution: 伽玛分布
  • weibull_distribution: 威布尔分布
  • extreme_value_distribution: 极值分配
  • normal_distribution: 正态分布
  • lognormal_distribution: 对数正态分布
  • chi_squared_distribution: 卡方分布
  • cauchy_distribution: 柯西分布
  • fisher_f_distribution: Fisher F分布
  • student_t_distribution: 学生T分布
  • discrete_distribution: 离散分布
  • piecewise_constant_distribution: 分段常数分布
  • piecewise_linear_distribution: 分段线性分布

下面举个生成器和分布器组合生成随机常用例子,以下为模拟掷骰子生成点数的实现:

#include <iostream>
#include <random>

int main()
{
    std::mt19937 gen(std::random_device{}());
    std::uniform_int_distribution<> dist(1, 6);

    for (int i = 0; i < 10; ++i)
        std::cout << dist(gen) << std::endl;

    return 0;
}

编译运行结果如下:

3
2
4
1
5
4
1
1
3
4

7.注意事项

  1. 范围std::shuffle 会打乱 [first, last) 的元素。在调用前,请确保迭代器范围合法且在可随机访问的容器内。

  2. 随机引擎种子:如果每次都用相同的种子,那么洗牌的结果也将完全相同。因此,如果需要每次运行程序都得到不同的洗牌结果,建议使用 std::random_device 或时间戳等方式进行初始化。

  3. 算法复杂度std::shuffle 实现了线性复杂度(O(n))的洗牌算法(Fisher–Yates shuffle 或 Knuth shuffle),不会随着容器大小指数级增长。对于大规模数据,这点十分重要。

  4. 可移植性:当使用相同的随机引擎和相同的种子时,std::shuffle 洗牌结果在不同平台上应该是一致的(因为随机数引擎按标准定义了算法),但如果使用不同的编译器或与平台相关的自定义实现,则仍有可能出现微小差异。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值