随机抽奖算法实现与对比:聚焦洗牌算法(Fisher-Yates)

期末课程设计中,我和团队成员共同完成了 “随机抽奖算法实现与比较” 的课题。本次设计的核心目标是模拟实际抽奖场景,从指定号码范围(min_num 到 max_num)中抽取 k 个不重复的中奖号码,并通过实现四种不同算法,对比其效率、公平性与适用场景,为实际应用提供参考。其中,我主要负责洗牌算法(Fisher-Yates 版本)的设计与实现,这也是本次设计中公平性和效率兼具的核心算法之一。。

一、课题背景与核心要求

1. 场景描述

模拟实际抽奖活动,需从连续整数区间 [min_num, max_num] 中抽取 k 个不重复号码,核心是保证算法的合理性(无重复)、高效性(低时间 / 空间开销),同时兼顾不同场景的需求(如公平抽奖、偏向性抽奖等)。

2. 设计目标

实现四种随机抽奖算法,对比其时间复杂度、空间复杂度、优缺点及适用场景,最终形成可落地的技术方案。四种算法分别为:基础随机法、洗牌算法、加权随机法、批量随机法。

二、四种算法简要概述

在深入讲解洗牌算法前,先快速梳理另外三种算法的核心逻辑,方便对比理解:

算法名称核心思路关键特点
基础随机法逐个生成随机数,暴力遍历查重,重复则重生成逻辑最简单,但 k 接近总数时重复率高,效率 O (k²)
加权随机法为每个号码分配权重,按累积权重区间随机选择可实现偏向性抽奖(如新员工高概率中奖),时间复杂度 O (kn)
批量随机法一次生成 2k 个随机数,用哈希表批量去重平衡时间与空间,平均时间复杂度 O (k),通用场景优选
洗牌算法模拟洗牌发牌,先打乱全量号码池,再取前 k 个绝对公平、无重复,时间复杂度 O (n),效率高

三、洗牌算法(Fisher-Yates)详解

1. 算法核心思想

洗牌算法的灵感源于 “洗扑克牌 + 发牌”:先将所有可选号码([min_num, max_num])按顺序排成 “一摞牌”(号码池),然后通过随机交换位置的方式彻底打乱这摞牌,最后直接从打乱后的牌堆中取前 k 个号码,即为中奖结果。

核心优势:每个号码被选中的概率完全相等(绝对公平),且天然无重复,无需额外查重步骤。

2. 算法原理与流程

(1)核心原理

Fisher-Yates 洗牌算法的关键是 “从后往前遍历 + 随机交换”:

  • 遍历号码池时,从最后一个元素开始,每次为当前位置随机选择一个 “未被打乱的位置”(即索引范围 [0, 当前位置]);
  • 将当前位置的元素与随机位置的元素交换,确保每个元素被放到任意位置的概率均等;
  • 遍历结束后,号码池完全打乱,取前 k 个元素即可(也可取后 k 个,本质一致)。
(2)完整流程
  1. 参数合法性校验:判断 min_num > max_num、k ≤ 0 等非法情况,若非法直接返回空结果;
  2. 构建号码池:将 [min_num, max_num] 区间内的所有整数存入向量(vector),模拟 “一整副牌”;
  3. 容错处理:若 k ≥ 号码池总数(即要抽的数量≥可选数量),直接返回全部号码(全中);
  4. 核心洗牌:从号码池末尾(索引 n-1)向前遍历至索引 n-k,每次生成 [0, 当前索引] 的随机数,交换当前位置与随机位置的元素;
  5. 结果截取:截取打乱后号码池的最后 k 个元素(或前 k 个),作为中奖结果返回。

3. 代码实现(C++)

cpp

运行

#include <vector> #include <cstdlib> #include <algorithm> // for swap using namespace std; 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 核心洗牌逻辑(从后往前交换) for (int i = n - 1; i >= n - k; --i) { // 生成 [0, i] 范围内的随机索引 int rand_idx = rand() % (i + 1); // 交换当前位置与随机位置的元素 swap(pool[i], pool[rand_idx]); } // 5. 截取最后 k 个元素作为结果 vector<int> result(pool.end() - k, pool.end()); return result; }

4. 复杂度分析

  • 时间复杂度:O(n)构建号码池需遍历 n 个元素(O (n)),洗牌过程遍历 n 次(O (n)),截取结果为 O (k)(k ≤ n),总复杂度为线性阶 O (n),效率极高。
  • 空间复杂度:O(n)需要存储完整的号码池,空间消耗与号码总数 n 成正比,适合号码范围不大的场景(如 1-1000 抽奖)。

5. 关键优化与注意事项

  • 随机数种子:需在主函数中调用srand(time(nullptr)),确保每次运行程序的洗牌结果不同(避免固定中奖号码);
  • 公平性保障:“从后往前遍历 + 随机索引范围 [0, i]” 是 Fisher-Yates 算法的核心,若索引范围错误(如 [0, n-1]),会导致概率不均;
  • 边界处理:当 k=0 或 k 超过号码池总数时,直接返回空或全量号码,避免数组越界错误。

四、四种算法性能对比

为了更清晰地体现洗牌算法的优势,整理了四种算法的核心指标对比:

算法名称时间复杂度空间复杂度优点缺点适用场景
基础随机法O(k²)O(k)逻辑简单、易实现效率低,k 接近 n 时性能骤降小规模抽奖(如 k≤10,n≤50)
洗牌算法O(n)O(n)绝对公平、无重复、速度快内存占用与 n 成正比号码范围不大(n≤10000)、追求公平的场景
加权随机法O(kn)O(n+k)可自定义中奖概率实现复杂,效率中等偏向性抽奖(如年会老员工 / 新员工倾斜)
批量随机法平均 O (k)O(k)时间空间平衡、通用高效需调整批量大小参数大规模抽奖、对内存敏感的场景

五、课程设计收获与心得

1. 技术层面

  • 深入理解了 Fisher-Yates 洗牌算法的底层逻辑,掌握了 “公平随机” 的实现关键,学会了通过时间 / 空间复杂度分析算法优劣;
  • 提升了 C++ 编程实践能力,尤其是向量(vector)、哈希表(unordered_set)的使用,以及边界处理、参数校验等健壮性设计技巧;
  • 学会了 “从简单到复杂、逐步优化” 的设计思路:从基础随机法的暴力查重,到洗牌算法的无重复公平性,再到批量随机法的效率优化,每一步都对应实际场景的需求。

2. 思维层面

  • 深刻体会到 “没有最好的算法,只有最适合的算法”:洗牌算法虽公平高效,但在 n 极大(如 1-100000)的场景下,内存占用过高,此时批量随机法更优;
  • 团队协作中,明确了 “分工明确、互补共赢” 的重要性:不同成员负责不同算法,通过交叉测试发现问题,最终形成完整的对比方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1014910.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

打开软件出现找不到vcruntime140d.dll文件的情况 下载修复解决

在使用电脑系统时经常会出现丢失找不到某些文件的情况&#xff0c;由于很多常用软件都是采用 Microsoft Visual Studio 编写的&#xff0c;所以这类软件的运行需要依赖微软Visual C运行库&#xff0c;比如像 QQ、迅雷、Adobe 软件等等&#xff0c;如果没有安装VC运行库或者安装…

外卖订餐管理系统

实验五 外卖订餐管理系统 一、实验目的1.理解程序的基本概念——程序、变量、数据结构&#xff1b;2.学会使用顺序、选择、循环、跳转语句编写程序&#xff1b;3.学会使用数据和方法。二、实验内容开发一个外卖订餐系统&#xff0c;需要实现“我要订餐”、“查看餐袋”、“签收…

Level 2 → Level 3

用上一关获得的秘密ssh连接ssh -p 2220 bandit2@bandit.labs.overthewire.org使用ls,看到存在一个文件,使用cat ./--* 获得密码

软件缺少vbschs.dll文件 无法启动运行的情况 下载修复

在使用电脑系统时经常会出现丢失找不到某些文件的情况&#xff0c;由于很多常用软件都是采用 Microsoft Visual Studio 编写的&#xff0c;所以这类软件的运行需要依赖微软Visual C运行库&#xff0c;比如像 QQ、迅雷、Adobe 软件等等&#xff0c;如果没有安装VC运行库或者安装…

Flutter 混合开发实战:从 Add-to-App 到高性能双向通信的全栈集成方案

引言&#xff1a;为什么大厂都在用“混合开发”而非纯 Flutter&#xff1f;尽管 Flutter 具备跨平台能力&#xff0c;但在实际落地中&#xff0c;几乎没有任何大型 App 是 100% 纯 Flutter 构建的。原因显而易见&#xff1a;已有数百万行原生代码&#xff08;iOS/Android&#…

外设与接口:基于内核 gpio-keys 子系统的按键处理

1 基本原理 在 Linux 中&#xff0c;gpio-keys 是一个平台驱动&#xff08;Platform Driver&#xff09;&#xff0c;它充当了物理 GPIO 硬件与 Linux 标准输入子系统&#xff08;Input Subsystem&#xff09;之间的“翻译官”。 整个处理流程自下而上分为四层&#xff1a; 硬件…

sglang 大模型推理框架支持的EAGLE 1,2,3

文章目录EAGLE 系列模型的演进与核心机制关键参数与训练逻辑思考参考来源&#xff1a;https://docs.sglang.com.cn/backend/speculative_decoding.html https://github.com/SafeAILab/EAGLE EAGLE3 https://arxiv.org/pdf/2503.01840 EAGLE 系列模型的演进与核心机制 EAGLE 基…

延凡科技 EMS 智慧云平台:3 万起订阅,中小用能单位的 “云端全能源管家”

延凡科技 EMS 智慧云平台是专为工厂、园区、楼宇、医院等中小用能单位打造的云原生能源管理解决方案&#xff0c;聚焦 “降本节能、碳排合规、云端运维、数据驱动” 核心目标&#xff0c;采用 SaaS 订阅模式&#xff0c;整合物联网感知、云边协同、AI 能效优化算法&#xff0c;…

拦截器注册InterceptorRegistry 实现讲解

1.核心概念InterceptorRegistry 是 Spring MVC 提供的拦截器注册器&#xff0c;用于配置拦截器的拦截规则。2.主要方法addInterceptor(): 添加拦截器 addPathPatterns(): 指定要拦截的路径 excludePathPatterns(): 指定要排除的路径 路径匹配规则 /api/**: 匹配 /api/ 下的所有…

汇编语言全接触-27.工具提示控件

我们将学习工具提示控件:它是什么如何创建和使用.下载例子理论:工具提示是当鼠标在某特定区域上停留时显示的一个矩形窗口.工具提示窗口包含一些编程者想要显示的文本.在这点上,工具提示同状态栏的作用是一样的,所不同的是工具提示当单击或者远离指定区域的时候就会消逝,你可能…

汇编语言全接触-26.启动画面

上一章我们学习了位图的使用.在这一章我们要用上帝赋予我们的创造力来融会贯通上一章我们学到的知识.那就是研究如何用位图来创建启动画面. 你可以在这里下载示范: the example. 理论首先,我们先要搞清楚什么是启动画面.举个简单的例子:我们启动某些作的专业一点的程序时(比如N…

验证IP地址(一)

我们先来看题目描述&#xff1a;给定两个 没有重复元素 的数组 nums1 和 nums2 &#xff0c;其中nums1 是 nums2 的子集。找到 nums1 中每个元素在 nums2 中的下一个比其大的值。nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不…

医院管理|基于springboot 医院管理系统(源码+数据库+文档)

医院管理 目录 基于springboot vue医院管理系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue医院管理系统 一、前言 博主介绍&#xff1a;✌️大…

浅谈:算法中的斐波那契数(一)

我们先来看题目描述&#xff1a;斐波那契数&#xff0c;通常用 F(n) 表示&#xff0c;形成的序列称为斐波那契数列。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a;F(0) 0, F(1) 1 F(N) F(N - 1) F(N - 2), 其中 N > 1.给…

测试的“元认知”:智能体如何评估自身可靠性?

在软件测试领域&#xff0c;自动化与智能化正以前所未有的速度重塑工作流程。随着人工智能代理&#xff08;智能体&#xff09;广泛应用于测试用例生成、缺陷预测和持续集成&#xff0c;一个关键问题浮出水面&#xff1a;这些智能体如何像人类测试专家一样&#xff0c;对自身行…

10.8 总结

10.8 总结 作业回顾 1.1 索引练习节选 s hello 1 world 2 hello 3 Python # 获取s的长度 print(len(s)) # 30 # 获取第4个字符 print(s[3]) # l # 获取最后一个字符 print(s[-1]) # n # 获取第7个字符 print(s[6]) # 1 # 获取倒数第7个字符 print(s[-7]) # 空格【不显…

【Hadoop+Spark+python毕设】物联网网络安全威胁数据分析系统、计算机毕业设计、包括数据爬取、数据分析、数据可视化、Hadoop、实战教学

&#x1f393; 作者&#xff1a;计算机毕设小月哥 | 软件开发专家 &#x1f5a5;️ 简介&#xff1a;8年计算机软件程序开发经验。精通Java、Python、微信小程序、安卓、大数据、PHP、.NET|C#、Golang等技术栈。 &#x1f6e0;️ 专业服务 &#x1f6e0;️ 需求定制化开发源码提…

9.28总结

9.28总结 知识回顾 # 1. 封装一个函数&#xff1a;获取指定数据的阶乘 【没有指定数据的话默认求10的阶乘】 默认参数 # 阶乘 比如5&#xff01;5*4*3*2*1 # 未知数据 有1个 # 是否需要返回结果 def factorial(num10):result 1for i in range(num, 0, -1):result * ireturn…

零基础学JAVA--Day34(Map接口+HashTable+HashMap+TreeSet+TreeMap+开发中如何选择集合实现类?(重要)) - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …