news 2026/9/20 6:54:14

公平抽签算法实现:Fisher-Yates与蓄水池抽样详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
公平抽签算法实现:Fisher-Yates与蓄水池抽样详解

1. 问题背景与需求分析

公平抽签是一个在各类竞赛、活动组织中常见的实际问题。假设有N个人参与抽签,需要从中选出M个人,如何设计一个算法确保每个人被选中的概率完全相同?这个问题看似简单,但在实际编程实现中需要考虑诸多细节。

我在组织校园编程竞赛时曾遇到过类似需求:从120名报名者中随机抽取30名参赛者。最初用Excel随机排序发现边缘位置出现概率偏差,后来改用算法实现才解决公平性问题。这正是这类问题的典型应用场景。

2. 核心算法解析

2.1 洗牌算法(Fisher-Yates Shuffle)

最经典的解决方案是Fisher-Yates洗牌算法,其核心思想是通过倒序交换实现均匀排列:

import random def fair_draw(all_candidates, select_num): n = len(all_candidates) for i in range(n-1, n-select_num-1, -1): j = random.randint(0, i) all_candidates[i], all_candidates[j] = all_candidates[j], all_candidates[i] return all_candidates[-select_num:]

算法时间复杂度为O(M),空间复杂度O(1)。关键点在于:

  1. 从后向前遍历
  2. 随机范围逐步缩小
  3. 原地交换元素

2.2 蓄水池抽样算法

当N很大(如百万级)而M较小时,更优的方案是蓄水池抽样:

import random def reservoir_sampling(data_stream, k): reservoir = data_stream[:k] for i in range(k, len(data_stream)): j = random.randint(0, i) if j < k: reservoir[j] = data_stream[i] return reservoir

该算法特点:

  • 只需单次遍历数据
  • 不需要预先知道总数据量
  • 每个元素被选中的概率严格相等

3. 实现细节与边界处理

3.1 随机数生成的质量

常见误区是使用低质量随机源:

# 不推荐写法(伪随机性不足) random.seed(time.time()) % 1000

推荐做法:

# 使用系统级随机源(Linux) with open('/dev/urandom', 'rb') as f: seed = int.from_bytes(f.read(4), 'big') random.seed(seed)

3.2 重复抽签问题处理

当需要多次执行抽签时,需确保各次结果独立:

# 错误示例(随机状态污染) results = [fair_draw(data, 10) for _ in range(5)] # 可能产生关联 # 正确做法 def batch_draw(data, m, times): return [fair_draw(data.copy(), m) for _ in range(times)]

4. 概率验证与测试方法

4.1 蒙特卡洛测试

通过大量重复实验验证概率分布:

from collections import defaultdict def probability_test(n, m, trials=10000): counts = defaultdict(int) for _ in range(trials): sample = fair_draw(list(range(n)), m) for x in sample: counts[x] += 1 return {k: v/trials for k, v in counts.items()}

4.2 卡方检验

统计检验各元素出现频率是否均匀:

from scipy.stats import chisquare def chi2_test(n, m, trials=10000): probs = probability_test(n, m, trials) observed = list(probs.values()) expected = [m/n]*n return chisquare(observed, f_exp=expected)

5. 性能优化实践

5.1 大规模数据优化

当N>1e6时,内存优化方案:

def streaming_draw(iterator, m): reservoir = [] for i, item in enumerate(iterator): if i < m: reservoir.append(item) else: r = random.randint(0, i) if r < m: reservoir[r] = item return reservoir

5.2 并行化实现

多线程加速方案:

from multiprocessing import Pool def parallel_draw(data, m, workers=4): chunk_size = len(data) // workers with Pool(workers) as p: chunks = [data[i*chunk_size:(i+1)*chunk_size] for i in range(workers)] samples = p.starmap(fair_draw, [(chunk, m//workers) for chunk in chunks]) return [item for sublist in samples for item in sublist]

6. 实际应用案例

6.1 在线考试系统组卷

在某在线教育平台的组卷系统中,我们实现了这样的抽题逻辑:

def select_questions(question_bank, counts): result = {} for type_, (total, need) in counts.items(): pool = question_bank[type_] selected = fair_draw(pool, need) result[type_] = selected return result

6.2 会议抽奖系统

大型年会抽奖系统的关键实现:

class LotterySystem: def __init__(self, participants): self.pool = participants self.backup = participants.copy() def draw(self, n): if n > len(self.pool): self.pool = self.backup.copy() winners = fair_draw(self.pool, n) for winner in winners: self.pool.remove(winner) return winners

7. 常见问题与解决方案

7.1 随机性不足问题

现象:在小样本量时出现明显聚集解决方案

  1. 使用密码学安全随机源
  2. 增加熵池混合
import secrets def secure_shuffle(items): n = len(items) for i in range(n-1, 0, -1): j = secrets.randbelow(i+1) items[i], items[j] = items[j], items[i] return items

7.2 重复元素处理

当列表中存在重复元素时,需要特殊处理:

def dedup_draw(items, m): unique = list(set(items)) if len(unique) < m: raise ValueError("Not enough distinct items") return fair_draw(unique, m)

8. 算法扩展与变种

8.1 加权随机抽样

某些场景需要按权重抽样:

import bisect import random def weighted_sample(items, weights, m): cumulative = [] total = 0 for w in weights: total += w cumulative.append(total) result = [] for _ in range(m): r = random.uniform(0, total) idx = bisect.bisect_left(cumulative, r) result.append(items[idx]) return result

8.2 分层抽样

当数据具有明显分层结构时:

def stratified_sample(data, strata, m): strata_counts = {k: len(v) for k, v in data.items()} total = sum(strata_counts.values()) samples = [] for stratum, items in data.items(): stratum_m = max(1, round(m * strata_counts[stratum] / total)) samples.extend(fair_draw(items, stratum_m)) return fair_draw(samples, m)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 6:50:53

如何把网页视频存到本地:猫抓资源嗅探、M3U8 解析完整教程

如何把网页视频存到本地&#xff1a;猫抓资源嗅探、M3U8 解析完整教程 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 想保存网页视频&#xff0c;…

作者头像 李华
网站建设 2026/9/20 6:47:35

Three.js加载器全解析:从基础使用到高级优化

1. Three.js加载器深度解析&#xff1a;从基础到高级应用作为一名长期使用Three.js进行Web3D开发的工程师&#xff0c;我深知资源加载是项目开发中最关键的环节之一。本文将系统性地介绍Three.js中的各类加载器&#xff08;Loaders&#xff09;&#xff0c;分享我在实际项目中积…

作者头像 李华
网站建设 2026/9/20 6:43:54

把 Codex 的 Base URL 改到 TaoToken 之后,再装 Codex Dream Skin

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 6:41:57

自建开源研究流水线:从信息采集到知识输出的完整设计

1. 拆解OpenResearch&#xff1a;一套开源研究流水线的设计思路OpenResearch这个词&#xff0c;听起来像是某个大厂的内部代号&#xff0c;但对我来说&#xff0c;它其实是一个更朴素的东西&#xff1a;一套完全由开源工具拼起来的个人研究操作系统。我大概是去年年底开始动手搭…

作者头像 李华