news 2026/11/3 14:45:22

LeetCode 710题解析:高效处理黑名单随机数生成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 710题解析:高效处理黑名单随机数生成

1. 问题背景与需求拆解

LeetCode 710题"黑名单中的随机数"是一个经典的算法设计问题。题目要求我们实现一个类,能够高效地从[0, n-1]范围内随机返回一个不在黑名单中的数字。具体来说,给定整数n和一个无重复元素的blacklist数组,需要实现以下功能:

  • 构造函数Solution(int n, int[] blacklist):初始化随机数生成器
  • pick():每次调用时返回一个均匀随机的有效数字

这个问题的难点在于如何高效处理黑名单,特别是在n很大而blacklist相对较小的情况下。直接存储所有有效数字会消耗O(n)空间,这在n很大时显然不可行。

2. 核心算法思路分析

2.1 哈希映射重构法

最优解采用了一种巧妙的映射策略,核心思想是将黑名单数字"重映射"到白名单区域。具体步骤如下:

  1. 确定白名单数字的个数m = n - blacklist.length
  2. 将所有≥m的黑名单数字收集到一个集合中
  3. 建立映射关系:对于<m的黑名单数字,将它们映射到一个≥m的非黑名单数字

这样,我们只需要维护一个大小与blacklist相同的哈希表,就能在O(1)时间内完成随机数的生成和校验。

2.2 算法正确性证明

这种方法的正确性基于以下观察:

  • 白名单数字分为两部分:<m的非黑名单数字和≥m的非黑名单数字
  • 对于任何随机数r∈[0,m-1],如果r不在黑名单中,直接返回;否则返回其映射值
  • 由于映射保证了每个有效数字被选中的概率相同,因此满足均匀随机的要求

3. Java实现详解

import java.util.HashMap; import java.util.Map; import java.util.Random; class Solution { private final int m; private final Random random; private final Map<Integer, Integer> map; public Solution(int n, int[] blacklist) { this.m = n - blacklist.length; this.random = new Random(); this.map = new HashMap<>(); // 记录所有≥m的黑名单数字 Set<Integer> blacklistAboveM = new HashSet<>(); for (int num : blacklist) { if (num >= m) { blacklistAboveM.add(num); } } // 建立映射关系 int candidate = m; for (int num : blacklist) { if (num < m) { while (blacklistAboveM.contains(candidate)) { candidate++; } map.put(num, candidate); candidate++; } } } public int pick() { int r = random.nextInt(m); return map.getOrDefault(r, r); } }

3.1 构造函数解析

构造函数主要完成三部分工作:

  1. 计算白名单大小m = n - blacklist.length
  2. 收集所有≥m的黑名单数字到集合中
  3. 为每个<m的黑名单数字寻找一个≥m的非黑名单数字作为映射目标

注意:映射过程使用了一个自增的candidate指针,确保每个黑名单数字都能找到唯一的映射目标。

3.2 pick()方法解析

pick()方法的实现非常简洁:

  1. 生成[0,m-1]范围内的随机数
  2. 如果该数在映射表中,返回其映射值;否则直接返回该数

这种方法保证了:

  • 时间复杂度:O(1)的pick操作
  • 空间复杂度:O(B),其中B是blacklist的长度

4. 性能优化与边界处理

4.1 随机数生成优化

在实际应用中,可以考虑使用更高质量的随机数生成器,如:

private final ThreadLocalRandom random = ThreadLocalRandom.current();

这在多线程环境下能提供更好的性能。

4.2 边界条件处理

需要特别注意以下边界情况:

  1. n <= 0时应该抛出异常
  2. blacklist包含重复元素时需要去重
  3. blacklist长度≥n时无解

改进后的构造函数可以添加参数校验:

public Solution(int n, int[] blacklist) { if (n <= 0) throw new IllegalArgumentException("n must be positive"); // 其余初始化代码... }

5. 实际应用场景扩展

这种黑名单随机数生成技术在以下场景有实际应用:

  1. 抽奖系统:排除某些不符合条件的用户ID
  2. AB测试:从用户池中随机选取测试组,排除已参与其他实验的用户
  3. 游戏开发:随机生成不重复的道具或敌人

例如,在抽奖系统中可以这样使用:

int totalUsers = 1000000; int[] excludedUsers = getExcludedUsers(); // 获取黑名单 Solution raffle = new Solution(totalUsers, excludedUsers); int winner = raffle.pick(); // 公平随机选出获奖者

6. 常见问题与调试技巧

6.1 为什么映射要从m开始?

因为前m个数字中可能有黑名单数字,我们需要将它们映射到后面的非黑名单数字。这样可以保证:

  • 前m个数字中的白名单数字保持不变
  • 前m个数字中的黑名单数字被均匀映射到后面的白名单区域

6.2 如何处理非常大的n值?

当n非常大时(如1e9),算法仍然有效,因为:

  • 空间复杂度只与blacklist大小有关
  • 初始化时间与blacklist大小成正比
  • pick()操作始终保持O(1)时间复杂度

6.3 调试技巧

在开发过程中可以添加验证逻辑:

public int pick() { int r = random.nextInt(m); int result = map.getOrDefault(r, r); if (blacklistSet.contains(result)) { throw new IllegalStateException("Generated number is in blacklist!"); } return result; }

7. 算法变种与扩展思考

7.1 加权随机版本

如果需要支持不同数字的不同权重,可以结合别名方法(alias method):

  1. 为白名单数字分配权重
  2. 构建别名表
  3. 修改pick()方法实现加权随机

7.2 动态黑名单

如果黑名单可能动态变化,可以考虑:

  1. 使用更复杂的数据结构如跳表
  2. 采用惰性重映射策略
  3. 牺牲部分时间效率换取动态更新能力

7.3 分布式环境实现

在分布式系统中,可以:

  1. 使用一致性哈希确定节点负责的区间
  2. 每个节点维护自己的黑名单映射
  3. 通过协调器保证全局一致性

8. 工程实践建议

在实际项目中实现类似功能时,建议:

  1. 添加日志:记录初始化参数和映射关系,便于调试
  2. 性能监控:跟踪pick()方法的调用耗时
  3. 单元测试:覆盖各种边界条件
  4. 文档注释:清晰说明算法原理和使用约束

示例测试用例:

@Test public void testSolution() { int n = 10; int[] blacklist = {3, 5, 7, 9}; Solution solution = new Solution(n, blacklist); Set<Integer> results = new HashSet<>(); for (int i = 0; i < 1000; i++) { int num = solution.pick(); assertFalse(Arrays.asList(3, 5, 7, 9).contains(num)); results.add(num); } // 验证所有白名单数字都可能被选中 assertEquals(6, results.size()); }

9. 与其他随机数问题的对比

与普通随机数生成相比,这个问题有几个特殊之处:

  1. 范围不连续:有效数字可能是不连续的区间
  2. 静态黑名单:初始化后黑名单不变
  3. 高效查询:需要支持高频的pick操作

类似问题还包括:

  • 带权重的随机选择(LeetCode 528)
  • 随机数索引(LeetCode 398)
  • 随机翻转矩阵(LeetCode 519)

10. 个人实现心得

在实际编码过程中,有几点特别值得注意:

  1. 映射构建顺序:先收集≥m的黑名单数字,再处理<m的部分,这个顺序很重要
  2. 候选数字选择:使用while循环寻找可用候选数字比预先生成所有候选更高效
  3. 随机数种子:在测试时固定随机种子便于重现问题

一个容易忽略的细节是blacklist可能包含n本身或更大的数字,虽然题目保证blacklist中的数字都在[0,n-1]范围内,但防御性编程建议添加校验:

for (int num : blacklist) { if (num >= n) throw new IllegalArgumentException("Blacklist number out of range"); }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/3 14:45:18

直流电机H桥驱动选型:分立MOS、双极性与L298效率对比

简介&#xff1a;这份文档面向制作智能小车、从事电子工程与机器人制作的读者&#xff0c;围绕有刷直流电机 H 桥驱动方案的选择展开分析&#xff0c;重点解答为何采用分立元件搭建 H 桥、为何不选 L298 集成桥、为何优先使用 MOS 管等常见困惑。内容从 H 桥正反转、刹车与惰行…

作者头像 李华
网站建设 2026/11/3 14:44:54

强模型降价潮下,如何约束过度思考降低推理成本

1. 一场“闪电战”背后的真实逻辑&#xff1a;为什么强模型反而先撑不住90分钟&#xff0c;两个顶级模型阵营同时放出新版本&#xff0c;价格却往下砍了一大截——这个场景放在两年前&#xff0c;几乎没人敢信。那会儿大家讨论的还是“谁的参数更多”“谁的榜单分更高”&#x…

作者头像 李华
网站建设 2026/11/3 14:44:53

openGauss逻辑存储结构解析与优化实践

1. 高斯数据库&#xff08;openGauss&#xff09;逻辑存储结构概述 openGauss作为华为开源的企业级关系型数据库&#xff0c;其逻辑存储结构设计直接决定了数据管理的效率和可靠性。在实际运维中&#xff0c;我曾遇到一个典型案例&#xff1a;某电商平台迁移至openGauss后&…

作者头像 李华
网站建设 2026/11/3 14:44:28

JSP在线课堂系统开发:实时互动与架构设计实践

1. 项目概述&#xff1a;JSP在线课堂互动教学系统的核心价值这个基于JSP的在线课堂系统本质上是一个B/S架构的实时教学平台。我去年为某职业培训机构实施过类似方案&#xff0c;其核心解决了传统线下教学的三个痛点&#xff1a;地域限制、互动匮乏和教学资源难以沉淀。系统采用…

作者头像 李华
网站建设 2026/11/3 14:44:23

AI工程从零到落地:系统入门路线与实战指南

1. 先想清楚&#xff1a;AI工程到底在解决什么问题1.1 为什么现在该重新理解AI工程说个比较实际的观察。这两年AI岗位的需求曲线变化很明显&#xff0c;真正缺的不是会调包跑模型的人&#xff0c;而是能把AI系统做扎实、能落地、能维护的人。所谓的AI engineer&#xff0c;说白…

作者头像 李华
网站建设 2026/11/3 14:43:35

后见之明偏差如何毁掉复盘?用时间线与决策记录破解

“hindsight is 20/20”&#xff0c;这句英文俗语我读第一遍的时候只觉得是在说“事后看一切清楚”。后来干了十几年研发&#xff0c;参与过几十场大大小小的复盘会&#xff0c;才真正理解“事后”这两个字的杀伤力。每当线上事故复盘&#xff0c;总有人拍桌子说“这个配置错误…

作者头像 李华