C++ 实现 4 种抽奖随机号码生成方案,从基础到加权全覆盖
2026/4/17 19:50:10 网站建设 项目流程

在各类线上活动、线下抽奖场景中,随机号码生成是核心功能之一。无论是简单的幸运大转盘,还是复杂的分层权重抽奖,都需要高效、公平(或可配置公平性)的随机号码生成逻辑。本文将基于 C++ 实现 4 种不同的抽奖随机号码生成方案,从基础的暴力去重到专业的加权随机,覆盖不同场景需求,并深入分析各方案的优缺点与适用场景,帮助开发者快速选型。

一、场景与需求定义

在开始代码解析前,先明确抽奖场景的核心需求:

  • 基础范围:从 min_num 到 max_num 的连续整数中抽取(如 1-50);
  • 结果要求:生成 k 个不重复的随机号码(如抽奖 6 个幸运数字);
  • 扩展需求:部分场景需支持「权重倾斜」(如特定号码中奖概率更高)、「高效生成」(如百万级号码池场景)。

基于以上需求,本文实现 4 种方案,分别应对不同场景,以下逐一展开。

二、4 种生成方案的完整实现与解析

所有方案均基于 C++ 标准库实现,无需第三方依赖,核心用到 vector(存储结果)、unordered_set(高效去重)、algorithm(算法工具)等组件,先统一初始化随机种子(srand((unsigned int)time(nullptr)))确保随机性。

方案 1:基础随机法(暴力查重)—— 最简单的入门方案

核心逻辑

逐个生成随机数,通过「遍历已选结果」判断是否重复,不重复则加入结果集,直到凑够 k 个号码。适合理解随机生成的基本逻辑,代码复杂度最低。

完整代码

#include <iostream>

#include <vector>

#include <ctime>

using namespace std;

// 基础随机法(暴力查重)

vector<int> randomDraw_basic(int min_num, int max_num, int k) {

vector<int> result;

// 1. 参数合法性校验:避免无效输入(如min>max、k<=0、k超过总号码数)

if (min_num > max_num || k <= 0 || k > (max_num - min_num + 1)) {

return result; // 无效输入返回空结果

}

int total_num = max_num - min_num + 1; // 总号码数

// 2. 循环生成不重复号码

while (result.size() < k) {

// 生成[min_num, max_num]范围内的随机数:rand()%总数 + 最小值

int num = rand() % total_num + min_num;

bool is_duplicate = false;

// 3. 暴力遍历查重(时间复杂度O(k))

for (size_t i = 0; i < result.size(); ++i) {

if (result[i] == num) {

is_duplicate = true;

break;

}

}

// 4. 无重复则加入结果

if (!is_duplicate) {

result.push_back(num);

}

}

return result;

}

// 辅助打印函数(所有方案通用)

void printResult(const vector<int>& nums, const string& method_name) {

cout << "【" << method_name << "】中奖号码:";

for (size_t i = 0; i < nums.size(); ++i) {

cout << nums[i];

if (i != nums.size() - 1) cout << "、";

}

cout << endl;

}

// 测试主函数

int main() {

srand((unsigned int)time(nullptr)); // 初始化随机种子(确保每次运行结果不同)

const int MIN_NUM = 1, MAX_NUM = 50, DRAW_COUNT = 6; // 1-50中抽6个

// 测试基础随机法

printResult(randomDraw_basic(MIN_NUM, MAX_NUM, DRAW_COUNT), "基础随机法");

return 0;

}

优缺点分析
  • 优点:逻辑简单,代码量少,无需额外数据结构,易上手;
  • 缺点:效率低,当 k 接近总号码数时,重复概率升高,需反复生成(如 1-100 抽 99 个,后期几乎每次生成都是重复);查重用遍历,时间复杂度 O(k²)。
  • 适用场景:小范围、小数量抽奖(如 1-20 抽 3 个),或仅用于学习理解基础逻辑。

方案 2:洗牌算法法(Fisher-Yates)—— 公平性最优方案

核心逻辑

先构建包含所有号码的「完整号码池」,再通过 Fisher-Yates 洗牌算法打乱顺序,最后截取前 k 个(或后 k 个)作为结果。Fisher-Yates 算法能保证每个号码被选中的概率完全相等,是公平抽奖的首选。

完整代码

#include <iostream>

#include <vector>

#include <algorithm> // 用于swap函数

#include <ctime>

using namespace std;

// 洗牌算法法(Fisher-Yates)

vector<int> randomDraw_shuffle(int min_num, int max_num, int k) {

vector<int> pool;

// 1. 参数合法性校验

if (min_num > max_num || k <= 0) {

return pool;

}

// 2. 构建完整号码池(存储min_num到max_num的所有数)

for (int i = min_num; i <= max_num; ++i) {

pool.push_back(i);

}

int n = pool.size(); // 号码池总数

// 3. 容错处理:若k≥总号码数,直接返回所有号码(避免无效截取)

if (k >= n) {

return pool;

}

// 4. Fisher-Yates核心洗牌逻辑:从后往前交换,仅需交换k次

for (int i = n - 1; i >= n - k; --i) {

// 生成0~i的随机索引(确保每次交换的范围合法)

int rand_idx = rand() % (i + 1);

// 交换当前位置(i)与随机索引位置的号码

swap(pool[i], pool[rand_idx]);

}

// 5. 截取最后k个号码作为结果(因最后k次交换已确保这k个是随机的)

vector<int> result(pool.end() - k, pool.end());

return result;

}

// 辅助打印函数(同方案1)

void printResult(const vector<int>& nums, const string& method_name) {

cout << "【" << method_name << "】中奖号码:";

for (size_t i = 0; i < nums.size(); ++i) {

cout << nums[i];

if (i != nums.size() - 1) cout << "、";

}

cout << endl;

}

// 测试主函数

int main() {

srand((unsigned int)time(nullptr));

const int MIN_NUM = 1, MAX_NUM = 50, DRAW_COUNT = 6;

// 测试洗牌算法法

printResult(randomDraw_shuffle(MIN_NUM, MAX_NUM, DRAW_COUNT), "洗牌算法法");

return 0;

}

关键细节
  • Fisher-Yates 优化:传统洗牌需遍历所有元素(O(n) 次交换),本文优化为仅交换 k 次(从 n-1 到 n-k),减少计算量;
  • 公平性保证:每次交换时,随机索引的范围是 0~i,确保每个未被选中的号码都有平等机会被交换到当前位置,最终 k 个号码的概率完全一致。
优缺点分析
  • 优点:公平性最优,时间复杂度 O(n + k)(构建号码池 O(n),交换 O(k)),效率高;
  • 缺点:需存储完整号码池,当总号码数极大时(如 1-100 万),内存占用高(vector 需存储 100 万个 int,约 4MB,实际可接受,但需注意)。
  • 适用场景:中大范围、对公平性要求高的抽奖(如 1-1000 抽 10 个),是大部分场景的首选方案。

方案 3:加权随机法 —— 支持概率倾斜的灵活方案

核心逻辑

给每个号码分配一个「权重值」,权重越高,被选中的概率越大。通过「权重累加区间匹配」找到选中的号码,并用 unordered_set 高效去重,适合需要分层概率的场景(如会员中奖概率高于普通用户)。

完整代码

#include <iostream>

#include <vector>

#include <unordered_set> // 用于高效去重

#include <numeric> // 用于accumulate(计算权重总和)

#include <ctime>

using namespace std;

// 加权随机法(按权重调整概率)

vector<int> randomDraw_weighted(int min_num, int max_num, int k, const vector<double>& weights) {

vector<int> result;

unordered_set<int> used; // 记录已选号码,查重时间复杂度O(1)

// 1. 参数合法性校验:权重数组长度必须与总号码数一致

if (min_num > max_num || k <= 0 || weights.size() != (max_num - min_num + 1)) {

return result;

}

// 2. 计算权重总和(避免总权重为0导致无效随机)

double total_weight = accumulate(weights.begin(), weights.end(), 0.0);

if (total_weight <= 0) {

return result;

}

// 3. 按权重抽取不重复号码

while (result.size() < k) {

// 生成0~总权重的随机数(rand()/RAND_MAX将随机数归一到0~1)

double rand_weight = (rand() / (double)RAND_MAX) * total_weight;

double current_weight = 0.0;

int selected_num = -1;

// 4. 匹配权重区间:累加权重,直到超过随机权重值,找到对应号码

for (int i = 0; i < weights.size(); ++i) {

current_weight += weights[i];

if (current_weight >= rand_weight) {

selected_num = min_num + i; // 映射到实际号码(i是权重数组索引)

break;

}

}

// 5. 查重后加入结果(未被选中过且号码有效)

if (selected_num != -1 && used.find(selected_num) == used.end()) {

used.insert(selected_num);

result.push_back(selected_num);

}

}

return result;

}

// 辅助打印函数(同方案1)

void printResult(const vector<int>& nums, const string& method_name) {

cout << "【" << method_name << "】中奖号码:";

for (size_t i = 0; i < nums.size(); ++i) {

cout << nums[i];

if (i != nums.size() - 1) cout << "、";

}

cout << endl;

}

// 测试主函数

int main() {

srand((unsigned int)time(nullptr));

const int MIN_NUM = 1, MAX_NUM = 50, DRAW_COUNT = 6;

// 初始化权重:20-30号权重3(中奖概率高),其余号码权重1(正常概率)

vector<double> weights(MAX_NUM - MIN_NUM + 1, 1.0);

for (int i = 0; i < weights.size(); ++i) {

int num = MIN_NUM + i;

if (num >= 20 && num <= 30) {

weights[i] = 3.0;

}

}

// 测试加权随机法

printResult(randomDraw_weighted(MIN_NUM, MAX_NUM, DRAW_COUNT, weights), "加权随机法");

return 0;

}

关键细节
  • 权重区间匹配:例如总权重为 W,生成随机数 r(0~W),累加权重直到超过 r,对应的号码即为选中项。如 20-30 号权重 3,其余 1,总权重为 (50-1+1) - 11 + 11*3 = 50 + 22 = 72,20-30 号的选中概率是普通号码的 3 倍;
  • 高效去重:用 unordered_set 存储已选号码,查重时间复杂度 O(1),比方案 1 的遍历快得多。
优缺点分析
  • 优点:支持概率倾斜,灵活应对分层抽奖场景;去重高效;
  • 缺点:需额外传入权重数组,配置成本高;当 k 较大时,可能因重复生成导致循环次数增加(可通过优化权重动态调整解决,见下文优化方向)。
  • 适用场景:需要差异化概率的抽奖(如会员专属奖、特定号码偏好)。

方案 4:批量随机法 —— 高效去重的优化方案

核心逻辑

针对方案 1「逐个生成效率低」的问题,改为「批量生成随机数 + 哈希去重」,减少循环次数。例如每次生成 2*k 个随机数(批量大小可配置),一次性筛选出不重复的号码,适合大范围、大数量抽奖。

完整代码

#include <iostream>

#include <vector>

#include <unordered_set>

#include <ctime>

using namespace std;

// 批量随机法(高效去重)

vector<int> randomDraw_batch(int min_num, int max_num, int k) {

vector<int> result;

unordered_set<int> used; // 高效去重

// 1. 参数合法性校验

if (min_num > max_num || k <= 0 || k > (max_num - min_num + 1)) {

return result;

}

int total_num = max_num - min_num + 1;

const int BATCH_SIZE = k * 2; // 批量生成大小(经验值:k的2倍,可根据场景调整)

// 2. 批量生成+去重

while (result.size() < k) {

// 3. 批量生成BATCH_SIZE个随机数

vector<int> batch_nums;

for (int i = 0; i < BATCH_SIZE; ++i) {

int num = rand() % total_num + min_num;

batch_nums.push_back(num);

}

// 4. 批量筛选:未被选中过的号码加入结果

for (int num : batch_nums) {

if (used.find(num) == used.end() && result.size() < k) {

used.insert(num);

result.push_back(num);

}

}

}

return result;

}

// 辅助打印函数(同方案1)

void printResult(const vector<int>& nums, const string& method_name) {

cout << "【" << method_name << "】中奖号码:";

for (size_t i = 0; i < nums.size(); ++i) {

cout << nums[i];

if (i != nums.size() - 1) cout << "、";

}

cout << endl;

}

// 测试主函数

int main() {

srand((unsigned int)time(nullptr));

const int MIN_NUM = 1, MAX_NUM = 50, DRAW_COUNT = 6;

// 测试批量随机法

printResult(randomDraw_batch(MIN_NUM, MAX_NUM, DRAW_COUNT), "批量随机法");

return 0;

}

关键细节
  • 批量大小选择:BATCH_SIZE 设为 k*2 是经验值,若总号码数大(如 1-100 万),可适当减小(如 k*1.5);若总号码数小(如 1-20),可增大(如 k*3),避免批量生成过多重复数;
  • 内存优化:无需存储完整号码池,仅存储批量生成的数和结果,内存占用低(适合百万级号码池)。
优缺点分析
  • 优点:效率高于方案 1(批量生成减少循环次数,哈希去重快);内存占用低,适合大范围抽奖;
  • 缺点:批量大小需手动调整,若配置不当(如批量太小),可能仍需多次循环;公平性略逊于方案 2(但实际差异极小,可忽略)。
  • 适用场景:大范围、大数量抽奖(如 1-100 万抽 100 个),或内存有限的场景。

三、4 种方案的核心维度对比(选型指南)

为了帮助开发者快速选择合适的方案,以下从效率、公平性、内存、适用场景四个核心维度进行对比:

对比维度

基础随机法

洗牌算法法

加权随机法

批量随机法

时间复杂度

O (k²)(遍历查重)

O (n + k)(构建池 + 交换)

O (k*m)(m 为总号码数)

O(k*BATCH_SIZE)

空间复杂度

O (k)(仅存结果)

O (n)(完整号码池)

O (n + k)(权重 + 结果 + 集合)

O(k + BATCH_SIZE)

随机性公平性

公平

最优公平(推荐)

可配置(非公平 / 公平)

公平(差异极小)

核心优势

逻辑简单

公平性高、效率均衡

支持权重倾斜

高效、省内存

核心劣势

效率低

耗内存(大号码池)

需配置权重

批量大小需调优

适用场景

小范围、小数量学习用

中大范围、公平性要求高

差异化概率抽奖

大范围、大数量、内存紧

四、优化方向与扩展建议

以上方案可根据实际需求进一步优化,提升性能或扩展功能:

1. 随机种子优化

当前用 time(nullptr) 作为随机种子,若短时间内多次调用(如 1 秒内调用多次),会导致随机种子相同,生成相同结果。优化方案:

  • 结合 clock()(时钟周期)或 getpid()(进程 ID):srand((unsigned int)(time(nullptr) ^ clock() ^ getpid())),增强随机性;
  • 若需更高安全性(如彩票类场景),可使用 C++11 的 random 库(如 std::mt19937 随机数生成器),替代 rand()(rand() 随机性较弱)。

2. 加权随机法优化

当前选中号码后,权重未动态调整(已选号码仍可能被重复生成,浪费循环)。优化方案:

  • 选中号码后,将其权重设为 0,并重新计算总权重,避免后续重复生成该号码,减少循环次数;
  • 用「前缀和数组」预处理权重,将权重区间匹配的时间复杂度从 O(m) 降为 O(log m)(适合总号码数极大的场景)。

3. 批量随机法批量大小自适应

当前 BATCH_SIZE 是固定值,可改为自适应调整:

  • 根据「已选号码数 / 总号码数」动态调整批量大小:若已选号码少(如占比 < 10%),用 k*1.5;若已选号码多(如占比 > 50%),用 k*3,避免批量生成过多重复数。

4. 功能扩展

  • 排序功能:抽奖结果通常需要按升序排列,可在生成结果后调用 sort(result.begin(), result.end());
  • 多线程安全:若在多线程环境下使用,需给随机生成逻辑加锁(如 std::mutex),避免并发冲突;
  • 结果去重持久化:若需避免同一用户多次中奖,可将已中奖号码存入数据库或缓存,生成前先校验。

五、总结

本文实现的 4 种抽奖随机号码生成方案,覆盖了从基础到进阶的不同场景需求:

  • 若追求公平性,优先选「洗牌算法法」;
  • 若需差异化概率,选「加权随机法」;
  • 若需高效省内存(大范围抽奖),选「批量随机法」;
  • 若仅用于学习理解,选「基础随机法」。

所有方案均已提供完整可运行代码,开发者可根据实际场景选型,并结合优化方向进一步提升性能。若有其他需求(如多语言实现、分布式抽奖),可在评论区交流讨论!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询