目录
3.std::shuffle 与 std::random_shuffle 的区别
1.简介
std::shuffle
是 C++ 标准库中用于对序列进行随机重排(洗牌)的一种算法。它可以将容器(例如 std::vector
、std::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::vector
、std::array
或内置数组的指针等。 -
URBG:Uniform Random Bit Generator,即随机数引擎类型。比如常用的
std::default_random_engine
、std::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.注意事项
-
范围:
std::shuffle
会打乱[first, last)
的元素。在调用前,请确保迭代器范围合法且在可随机访问的容器内。 -
随机引擎种子:如果每次都用相同的种子,那么洗牌的结果也将完全相同。因此,如果需要每次运行程序都得到不同的洗牌结果,建议使用
std::random_device
或时间戳等方式进行初始化。 -
算法复杂度:
std::shuffle
实现了线性复杂度(O(n))的洗牌算法(Fisher–Yates shuffle 或 Knuth shuffle),不会随着容器大小指数级增长。对于大规模数据,这点十分重要。 -
可移植性:当使用相同的随机引擎和相同的种子时,
std::shuffle
洗牌结果在不同平台上应该是一致的(因为随机数引擎按标准定义了算法),但如果使用不同的编译器或与平台相关的自定义实现,则仍有可能出现微小差异。