发布时间:2026/9/7 22:36:15
用rand7实现rand10:拒绝采样原理、优化与常见误区 刷题刷到随机数这块LeetCode 470 基本是绕不开的一道题。题目很短一句话给你一个能等概率生成 1 到 7 的 rand7()要求只基于它实现 rand10()让输出 1 到 10 也等概率。很多人第一眼觉得应该很简单写出来的代码却经不起推敲。我最早刷这道题的时候也想当然地写过(rand7() rand7()) % 10 1这种写法被测试数据教育了之后才老老实实去查为什么。后来在面试里也遇到过几次这道题的变体比如用 rand5() 生成 rand7()本质上都和拒绝采样有关。所以这篇文章我把自己从错误到正确、从基础到优化的完整思路写出来顺便把常见误区和证明方法也整理清楚希望能帮你一次吃透这道经典概率题。1. 题目到底在考什么为什么不能简单硬凑1.1 单次调用的信息量不够先从最底层想这个问题。rand7()的输出只有 7 种可能而目标rand10()需要 10 种可能。一次rand7()无论如何也没法直接变出 10 种等概率结果因为 7 不是 10 的倍数。这句话看着像废话但很多人写错代码就是因为忽略了这一点。有人会想那我把rand7()的结果乘一个系数不就行了比如rand7() * 10 // 7。问题是乘法缩放只能放大数值区间不能改变每个值出现的概率更没法凭空多出几种等概率的结果来。rand7()的结果 1 到 7 本来就是等概率的经过线性变换后还是 7 个等概率的值不可能均匀覆盖 1 到 10 这 10 个整数。所以核心思路只能是多次调用rand7()用它们的组合结果来构造一个更大的、仍然是等概率的样本空间然后从这个大空间里提取目标结果。这里的组合方式必须保证每个组合出现的概率完全相等否则后面全是白搭。1.2 组合二维坐标把 1 到 49 当成一副等概率卡牌常见的组合方式是调用两次rand7()。假设第一次叫a第二次叫b那么(a, b)一共有 7×749 种组合而且每种组合出现的概率都是 1/49。看起来很简单但怎么把 49 种等概率组合映射成 10 种等概率结果一个很自然的做法是构造一个公式num (a - 1) * 7 b这个num的取值范围是 1 到 49。你可以把它理解成把a当作“高位”b当作“低位”用 7 进制的方式拼成一个整数。具体来说当a1时num取 1 到 7当a2时num取 8 到 14依此类推每一个从 1 到 49 的整数都恰好对应一个唯一的(a, b)组合。这和“骰子掷出点数组合”是一个道理。49 种组合等概率等于你有 49 张编号 1 到 49 的卡牌随便抽一张。现在目标是从 1 到 49 这 49 个等概率的数字中得到均匀的 1 到 10。有人说那我直接对 10 取模不就行了num % 10 1看起来能覆盖 1 到 10但注意 49 和 10 不是倍数关系取模之后不同余数对应的原始数字个数不一样结果一定不均匀。这一点后面会专门讲。既然 49 不能被 10 整除那就必须丢掉一部分结果。丢掉谁、怎么丢就引出了拒绝采样的核心思想。2. 基础解法两次 Rand7() 加拒绝采样2.1 Python3 代码与逐行解释最简单的解法是这样def rand10(): while True: num (rand7() - 1) * 7 rand7() # 均匀生成 1..49 if num 40: return (num - 1) % 10 1逐行解释一下(rand7() - 1) * 7 rand7()生成 1 到 49 的均匀整数。为什么均匀因为每个(a, b)组合概率相同而num和组合是一一对应的。判断num 40。为什么取 40因为 40 是 10 的倍数1 到 40 可以均匀地分成 10 组每组 4 个数字。数字 1 到 10 各出现 4 次。(num - 1) % 10 1把 1 到 40 均匀映射到 1 到 10。比如num为 1、11、21、31 时都返回 1num为 2、12、22、32 时都返回 2依此类推。如果num落在 41 到 49 之间说明抽到了“无效卡牌”直接重新循环再试一次。这个算法核心就是拒绝采样。我可以打个比方你手里有一副 49 张的公平牌组只有抽到前 40 张才算数抽到后 9 张就洗牌重抽。重抽不会破坏公平性因为每一次抽取都是独立且等概率的抽中“有效区域”的条件概率在每一轮完全相同所以最终返回的每个结果仍然是均匀的。2.2 期望调用次数的计算写题的时候经常会被问到“这个算法平均要调用多少次 rand7()”。这也是面试官喜欢追问的点。我们每次循环调用 2 次rand7()成功概率是 40/49。于是从循环次数来看期望循环次数是成功概率的倒数期望循环次数 1 / (40/49) 49/40所以期望调用rand7()的次数就是期望调用次数 2 × 49/40 49/20 2.45也就是说平均每生成一个 1 到 10 的结果大约要调用 2.45 次rand7()。这个数字不是每次固定的但如果你跑一百万次测试统计出来的平均值会非常接近 2.45。基础解法最大的优点是简单、容易解释清楚。只要能说清楚“为什么要拒绝 41 到 49”这个解法在面试里已经算合格了。但我自己刷题的时候总觉得那 9 个被丢掉的结果有点可惜毕竟它们本身也是等概率的于是又去研究了优化方案。3. 优化解法三段式拒绝采样3.1 拒绝掉的 9 种结果其实还能用基础解法丢掉 41 到 49 这 9 个数字但如果换个角度想当num落在 41 到 49 之间时num - 40得到的是 1 到 9这其实是一个均匀的rand9()。丢掉它等于把一个现成的rand9()扔了。那能不能把这个rand9()再和一次新的rand7()结合生成更大范围的均匀数字当然可以。rand9()和rand7()组合一共是 9×763 种等概率结果。63 比 10 大多了至少能取 60 个也就是 10 的倍数然后保留 3 个继续利用。再看如果第二轮也失败num - 60得到的是 1 到 3这又是一个均匀的rand3()。rand3()和rand7()组合一共是 3×721 种等概率结果取前 20 个正好又是 10 的倍数只剩 1 个无效结果需要重来。这就是三段式拒绝采样的思路每一层失败后不直接重来而是把失败分支的“残余均匀性”榨干一直用到实在榨不出来为止。3.2 Python3 优化代码代码如下def rand10(): while True: a rand7() b rand7() num (a - 1) * 7 b # 1..49 if num 40: return (num - 1) % 10 1 a num - 40 # 1..9等价于 rand9() b rand7() num (a - 1) * 7 b # 1..63 if num 60: return (num - 1) % 10 1 a num - 60 # 1..3等价于 rand3() b rand7() num (a - 1) * 7 b # 1..21 if num 20: return (num - 1) % 10 1这段代码里每一层的num都是均匀的关键在于a的取值始终是连续等概率的第一层若失败num是 41 到 49 均匀a num - 40就是 1 到 9 均匀。第二层a有 9 种可能b有 7 种可能组合出来的num (a-1)*7 b覆盖 1 到 63每个数恰好出现一次均匀。第二层若失败num只可能是 61、62、63对应的a num - 60是 1、2、3均匀。第三层a有 3 种可能b有 7 种可能组合出来 1 到 21 均匀取前 20 个映射最后一个失败后重新开始整个循环。整个过程的核心就是组合生成均匀整数截取“10 的整数倍”部分剩余部分再组合、再截取。3.3 期望调用次数从 2.45 次降到 2.1933 次优化版到底优化了多少我们需要算一下期望调用次数。设总期望调用次数为 E从第一层开始看第一层要调用 2 次rand7()。成功概率 40/49失败概率 9/49。如果失败进入第二层这时要额外调用 1 次rand7()。第二层成功概率 60/6320/21失败概率 3/631/21。如果第二层也失败进入第三层额外调用 1 次rand7()。第三层成功概率 20/21失败概率 1/21。如果第三层也失败就回到第一层重新来期望仍然是 E。于是可以列出递推方程E 2 (9/49) × [1 (1/21) × (1 (1/21) × E)]解得E 329/150 ≈ 2.1933对比基础版的 2.45优化版平均每次能省大约 0.26 次调用大概 10% 的收益。这个优化在 LeetCode 的测试用例上不会有明显感觉毕竟单次调用本身很快但如果你在做随机采样类的高频场景比如跑蒙特卡洛模拟、随机抽样生成这个差别就会被放大。注意优化版代码更长面试时一定要先讲清楚“每一层为什么是均匀的”再写代码。如果代码写出来了但解释不清楚均匀性面试官很可能会认为你是背的答案。4. 常见错误写法与均匀性证明4.1 两个典型错误写法的反例很多人第一反应是(rand7() rand7()) % 10 1这个写法看起来像模像样但均匀性一验证就崩。关键问题是两个rand7()的和并不是均匀分布。设s a b取值范围是 2 到 14。不同和值对应的组合数完全不同和值 s组合数2132435465768796105114123132141也就是说和为 8 的组合有 7 种和为 2 的组合只有 1 种。对这样的和值取模再 1不同输出值背后的组合数也必然不一样怎么可能均匀另一个常见错误是(rand7() * rand7()) % 10 1。乘法分布同样不是均匀的。比如乘积为 1 的情况只有(1,1)一种乘积为 2 的情况有(1,2)和(2,1)两种乘积为 4 的情况有(1,4)、(4,1)、(2,2)三种。取模之后每个余数对应的组合数也不相等。遇到这类写法最简单的验证办法是写个循环跑几十万次统计每个结果的频率一眼就能看出不均匀。这里我额外强调一点均匀性的本质是“每个输出值对应的原始组合数相同”。只要这一点不满足不管代码多简单、多直观它都是错的。4.2 如何严格证明输出是均匀的面试里被问到“怎么证明你的解法是均匀的”不要只说“显然均匀”。给出一个严谨一点的说法。以基础版为例。在返回结果之前程序实际上只会在1..49中截取1..40部分。num的 40 个值里每一个值出现的概率都是 1/49。映射时1 到 10 每个目标值对应 4 个不同num。所以条件在“本轮回合并成功返回”的情况下返回任意目标值 k 的概率都是 4/401/10。然后要处理“可能经过多轮才成功”的情况。每一轮的成功概率和条件分布完全一样失败的轮次只是重试不影响最终分布。所以整体来看返回任意目标值 k 的概率仍然等于 1/10。优化版的证明也同理。第一层返回时是从1..40均匀截取第二层返回时是从1..60均匀截取第三层返回时是从1..20均匀截取。每一层中任意目标值 k 对应的原始数字个数分别是 4、6、2而各自的总有效数字个数分别是 40、60、20条件概率都等于 1/10。各层之间互斥最终无条件概率自然也是 1/10。4.3 扩展任意 RandM() 构造 RandN() 的通用框架这类题目不只是考rand7()到rand10()稍微变形就成了一道新题但底层框架是通用的。如果 M ≥ N直接用拒绝采样先生成randM()取randM() ≤ N * (M // N)的部分映射到 1 到 N超出就重试。如果 M N先组合多次randM()构造一个更大的均匀整数空间使得M^k ≥ N。比如rand5()构造rand7()可以调用两次rand5()得到 25 种等概率结果取其中 21 个映射到 1 到 7剩下 4 个重试。如果空间非常大还可以像优化版那样把“剩余部分”递归利用减少重试次数。这里面有个有趣的延伸视角从信息论看rand7()每次携带约 log2(7) ≈ 2.807 bit 信息量而rand10()需要 log2(10) ≈ 3.322 bit所以理论上平均至少需要约 1.18 次rand7()才能生成一次rand10()。基础解法 2.45 次离理论下界还很远三段式优化 2.19 次已经进步了一些但依然不是最优。算法界有更复杂的方法能逼近理论下界只是面试和实际工程里很少需要那么极限的优化。5. 面试实战与刷题体会5.1 面试场上怎么答最加分实际面试时我不太建议一上来就甩优化代码。更稳的节奏是第一步先说思路。明确要构造等概率的大空间再说“因为 49 不是 10 的倍数所以要拒绝采样”。这句话能直接点出重点。第二步给基础版代码。一边写一边解释(rand7()-1)*7 rand7()为什么均匀以及为什么取 40。这个阶段如果能顺口说出期望调用次数49/20 ≈ 2.45基本就能让面试官满意。第三步如果面试官追问“能优化吗”再给出三段式版本。这时一定要先讲“拒绝掉的 9 个数并不是废料它们组成了一个均匀的 rand9()”把递推方程写出来、算出 2.1933 的期望。这属于明显的加分项。我自己当时在面试里碰到过类似题面试官其实不一定期待你写出优化版他更想确认你有没有真正理解均匀性和拒绝采样的原理。所以我建议优先保证基础版说得无懈可击再谈优化。5.2 一些个人习惯写这道题的时候有几个习惯我建议直接养成。第一写完算法题先验证分布。不要只盯着正确性还要随机跑一大轮统计频率。我用过的简单办法就是collections.Counter(rand10() for _ in range(100000))看一眼每个数字的计数是否接近 10000。如果某个数字明显偏多或偏少多半是映射逻辑出了问题。第二代码里不要硬编码魔法数字至少要写清楚每个数字的含义。比如 40 是7*7 - 960 是9*7 - 321 是3*7。写注释的时候把“为什么 40、60、20”标注清楚回头再看代码时不容易懵。第三这类题目的核心其实是“把两个独立均匀样本组合成一个更大的均匀样本”。一旦掌握了这个套路遇到rand3()生成rand5()、rand5()生成rand7()之类的变体都可以直接套框架而不是靠背答案。最后再说一个小技巧如果面试官要求“不允许无限循环”可以在代码里设置最大重试次数比如循环 100 次后强制返回一个兜底结果。虽然理论上有极小概率走到兜底但在工程上能避免极端情况下的死循环。当然LeetCode 原题没有这个限制直接while True就行。

相关新闻

2026/9/7 22:31:14

统一数据总线架构:解决云原生多总线并存痛点的实践指南

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

2026/9/7 23:51:48

Flutter设备守护进程启动失败解决方案

1. 问题背景与现象描述 最近在配置Flutter开发环境时,遇到了一个棘手的问题:Flutter Device Daemon启动失败。这个问题导致Android Studio无法识别连接的设备,严重影响了开发效率。具体表现为运行 flutter doctor 命令时,控制台…

2026/9/7 23:51:48

从望文生义到构词逻辑:中英文思维差异如何重塑认知

开头中文的“望文生义”能力,我用一个例子就能让你瞬间体验:“打电话”。中文使用者看到这三个字,脑海里立刻浮现出“拨号—接通—说话”的完整行为链条,甚至不需要刻意理解。但把它翻译成英文“call somebody”,拆开来…

2026/9/7 23:51:48

Electron 应用分发实战:打包、asar 归档与重新品牌化指南

Electron 应用分发实战:打包、asar 归档与重新品牌化指南 【免费下载链接】electron :electron: Build cross-platform desktop apps with JavaScript, HTML, and CSS 项目地址: https://gitcode.com/GitHub_Trending/el/electron 本篇技术指南围绕 Electron…

2026/9/7 23:51:48

从Excel记账到Python数据分析:家庭支出自动化统计实战指南

1. 从Excel记账到Python分析:我为什么迈出这一步1.1 记账四年,Excel总表越来越难伺候我家记账记了快四年,一直用的Excel。一开始确实够用——每月底花十几分钟把微信、支付宝的账单手工录入一张总表,再用SUMIF、SUMIFS这些函数按分…

2026/9/7 23:51:48

从TCP/IP协议族到Socket编程:高频报错排查与实战指南

先说一个结论:标准网络协议栈里并没有“TCPTP”这个协议。这个写法大概率是 TCP/IP 的误写,也有人会把“TCP 和 UDP”连在一起顺手打个 TCPTP 出来。这些年我带过不少刚入行的同事,新人在看网络编程资料时最常出现的字面混淆就是这个词。所以…

2026/9/7 23:46:47

用Python驱动COPASI:插件体系与批量参数扫描实战

1. 任务插件生态:COPASI 的功能单元不止是“按钮”COPASI 这类生化系统仿真软件,绝大多数用户的使用路径是:打开 GUI、加载或建一个模型、点 Time-Course 或 Steady-State、看结果、导图。这套流程在单次实验里够用,但当你面对“同…

2026/9/7 0:47:43

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/7 0:14:19

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/7 0:14:17

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/7 0:03:36

基于YOLOv8和PyQt5的麦穗稻穗检测识别系统设计与实现

这次我们来看一个把目标检测算法和桌面端工具结合得很典型的项目:基于 YOLOv8 PyQt5 的麦穗稻穗检测识别系统。这个项目本身不是新概念,但它的价值在于落地形态很完整。YOLOv8 负责核心的麦穗稻穗目标检测,PyQt5 负责提供可视化的桌面交互界…

2026/9/7 0:03:36

UL 1642锂电池安全标准全解析:测试项目、认证流程与避坑指南

简介:UL 1642是锂电池安全领域的重要规范,本中文版资源适合锂电池制造商、检测机构工程师及产品认证相关人员阅读,用于理解电池在设计与制造层面的安全要求、测试方法与合规要点。资源共1个PDF文件,压缩包大小834KB,便…

2026/9/7 0:03:36

BS EN 13814-1-2019游乐设施安全标准:设计与制造核心要点解析

简介:BS EN 13814-1:2019是英国采纳欧洲标准EN 13814-1:2019的正式版本,由BSI标准出版,重点规定游乐设施和游乐设备在设计与制造环节的安全准则,与BS EN 13814-2:2019、BS EN 13814-3:2019共同取代旧版BS EN 13814:2004。该标准面…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/7 22:45:59

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…