C++ 的随机整数采样
文章目录
- 1. 采样示例
- 2. 源码 random_number_sampler.cpp
- 3. 使用方式
1. 采样示例
无放回 without replacement 的随机整数采样,和有放回的随机整数采样,是两个常见的功能。 C++ 没有直接提供这个功能,std::sample 只用于无放回的采样,因此可以手动创建,让使用起来更方便。使用的效果如下。
-
无放回的采样示例。
从 [2025, 888, 666] 三个数中进行采样,得到一个随机数。当采样次数超过总体的数量之后,进行报错。运行结果如下图。
代码中实际的做法,是先将 3 个数打乱,再从后向前,一个一个地取出数据。
-
有放回的采样示例。
从范围 [6, 8] 直接进行采样。因为是有放回的方式,所以可以无限次采样。代码运行结果如下。
2. 源码 random_number_sampler.cpp
#include <algorithm>
#include <cstdlib>
#include <format>
#include <initializer_list>
#include <iostream>
#include <mutex>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>/*
一个随机采样的 class,包括有放回 with replacement 和无放回两种。
输入:一个范围,如 [6, 8] 。输出:采样出一个数值。
有放回采样允许无限次采样。
无放回采样在将所有数值采样结束后,再次采样将抛出异常。
*/
class RandomIntegerSampler {public:// 手动指定几个数值采样时,使用大括号初始化对象。explicit RandomIntegerSampler(std::initializer_list<int> arguments) : uniform_dist_(0, arguments.size() - 1){if (arguments.size() == 0) {throw std::range_error("Argument size must bigger than 0");}for (const auto& each : arguments) {population_.emplace_back(each);}shuffle_population();}// 在 [low, high] 范围内采样时,用小括号初始化对象。RandomIntegerSampler(int low, int high) : population_(high - low + 1), uniform_dist_(0, high - low){// 初始化 population_ 时,把 high 也包括进去,所以要加 1 。if (low > high) {std::string info{"Argument low must no bigger than high, bug got low= " + std::to_string(low) + "\t high= " + std::to_string(high)};throw std::range_error(info);}std::iota(population_.begin(), population_.end(), low);shuffle_population();}void show_population() {std::cout << "elements in container: ";std::string format_str = "{: >6}"; // 右对齐,每个输出占 6 个空格位置。for (const auto& each : population_) {std::cout << std::vformat(format_str, std::make_format_args(each));}std::cout << "\n";}// 该函数用于无放回 without replacement 的采样。int no_replacement() {std::scoped_lock lock{mtx_}; // 多线程时用 mutex 进行保护。if (population_.empty()) {throw std::length_error{"Empty population, no sampling allowed!"};}int a{population_.back()};population_.pop_back(); // 必须清除掉已采样数值。return a;}// 该函数用于有放回 with replacement 的采样。int with_replacement() {std::scoped_lock lock{mtx_}; // 多线程时用 mutex 进行保护。int index{uniform_dist_(unsigned_int_generator_)};return population_[index];}private:std::vector<int> population_{}; // 采样的总体。std::mutex mtx_;// 用于后续的有放回采样。std::mt19937_64 unsigned_int_generator_{}; // 准备均匀分布的随机无符号整数。std::uniform_int_distribution<> uniform_dist_{};void shuffle_population() {std::random_device rd_seed; // 准备随机种子。unsigned_int_generator_.seed(rd_seed()); // 第一次 seed ,用于 shuffle 。// 打乱数据,生成随机序列。std::shuffle(population_.begin(), population_.end(), unsigned_int_generator_);// 第二次 seed ,用于后续的采样,解决一个问题:如果 shuffle 时 vector 中的数量极大,后续采样随机性会降低。unsigned_int_generator_.seed(rd_seed()); }
};
void demo_no_replacement() {std::cout << "无放回的随机采样,3 个数值 [2025, 888, 666]\n";RandomIntegerSampler sampler{2025, 888, 666};sampler.show_population();for (int i = 0; i < 5; ++i) {std::cout << "sampled[" << i << "]= " << sampler.no_replacement() << "\n";}
}
void demo_no_replacement_range() {std::cout << "无放回的随机采样,数值范围 [6, 8]\n";RandomIntegerSampler sampler(9, 8);sampler.show_population();for (int i = 0; i < 5; ++i) {std::cout << "sampled[" << i << "]= " << sampler.no_replacement() << "\n";}
}
void demo_with_replacement_range() {std::cout << "有放回的随机采样,数值范围 [6, 8]\n";RandomIntegerSampler sampler(6, 8);sampler.show_population();for (int i = 0; i < 5; ++i) {std::cout << "sampled[" << i << "]= " << sampler.with_replacement() << "\n";}
}int main() {demo_no_replacement();// demo_no_replacement_range();// demo_with_replacement_range();return 0;
}
3. 使用方式
创建了上面的 RandomIntegerSampler 之后,采样就会更方便,使用方式如下:
RandomIntegerSampler sampler{2025, 888, 666};sampler.no_replacement(); // 无放回的采样,得到一个整数。sampler.with_replacement(); // 有放回的采样,得到一个整数。
—————————— 本文结束 ——————————