Article / 文章
LeetCode第398题:随机数索引
给你一个可能含有 重复元素 的整数数组 nums,请你随机输出给定的目标数字 target 的索引。你可以假设给定的数字一定存在于数组中。 实现 Solution 类: - Solution(int[] nums) 用数组 nums 初始化对象。 - int pick(int target) 从 nums 中选出一个满足 nums[i] == target
博客摘要:本文深入解析LeetCode第398题“随机数索引”,这是一道中等难度的设计题目,重点考查水塘抽样算法(Reservoir Sampling)的应用。文章从简单的哈希表存储到高级的水塘抽样算法,详细分析了两种解法的适用场景,并提供完整的代码实现。适合想要学习随机算法和概率编程的读者,帮助理解如何在有限空间内进行等概率随机选择。
题目描述
给你一个可能含有 重复元素 的整数数组 nums,请你随机输出给定的目标数字 target 的索引。你可以假设给定的数字一定存在于数组中。
实现 Solution 类:
Solution(int[] nums)用数组nums初始化对象。int pick(int target)从nums中选出一个满足nums[i] == target的随机索引i。如果存在多个有效的索引,则每个索引的返回概率应当相等。
示例:
输入
["Solution", "pick", "pick", "pick"]
[[[1, 2, 3, 3, 3]], [3], [1], [3]]
输出
[null, 4, 0, 2]
解释
Solution solution = new Solution([1, 2, 3, 3, 3]);
solution.pick(3); // 随机返回索引 2, 3 或者 4 之一。每个索引的返回概率应该相等。
solution.pick(1); // 返回 0 。因为只有 nums[0] 等于 1 。
solution.pick(3); // 随机返回索引 2, 3 或者 4 之一。每个索引的返回概率应该相等。
提示:
1 <= nums.length <= 2 * 10^4-2^31 <= nums[i] <= 2^31 - 1target是nums中的一个整数- 最多调用
pick函数10^4次
题目链接:LeetCode 398. 随机数索引
解题思路
这道题有两种主要的解题思路:
方法一:预处理 + 随机选择
在构造函数中预处理数组,将每个值对应的所有索引存储在哈希表中,然后在pick函数中随机选择一个索引。
方法二:水塘抽样算法(Reservoir Sampling)
每次调用pick函数时遍历数组,使用水塘抽样算法保证每个满足条件的索引都有相等的被选中概率。
算法原理
方法一:哈希表预处理
思路:
- 在构造函数中遍历数组,将每个值对应的所有索引存储在哈希表中
- 在
pick函数中,根据target找到对应的索引列表,随机选择一个返回
优势:pick函数的时间复杂度为O(1)
劣势:需要O(n)的额外空间存储所有索引
方法二:水塘抽样算法
核心思想: 对于数组中第i个等于target的元素,我们以1/i的概率选择它。
算法步骤:
- 初始化count = 0,result = -1
- 遍历数组,当遇到target时:
- count++
- 以1/count的概率更新result为当前索引
- 返回result
数学证明:
- 第1个target被选中的概率:1/1 × (1-1/2) × (1-1/3) × … = 1/n
- 第2个target被选中的概率:1/2 × (1-1/3) × (1-1/4) × … = 1/n
- 第k个target被选中的概率:1/k × (1-1/(k+1)) × … = 1/n
复杂度分析
方法一:哈希表预处理
- 时间复杂度:
- 构造函数:O(n)
- pick函数:O(1)
- 空间复杂度:O(n) - 存储所有索引
方法二:水塘抽样
- 时间复杂度:
- 构造函数:O(1)
- pick函数:O(n)
- 空间复杂度:O(1) - 只使用常数额外空间
图解思路
数组: [1, 2, 3, 3, 3]
索引: 0 1 2 3 4
方法1:哈希表预处理
{
1: [0],
2: [1],
3: [2, 3, 4]
}
pick(3) -> 从[2, 3, 4]中随机选择
方法2:水塘抽样
pick(3)的过程:
- i=2, nums[2]=3, count=1, 以1/1概率选择,result=2
- i=3, nums[3]=3, count=2, 以1/2概率选择,可能更新result=3
- i=4, nums[4]=3, count=3, 以1/3概率选择,可能更新result=4
水塘抽样过程示例
| 步骤 | 索引 | 值 | count | 随机数 | 选择概率 | 当前结果 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 1 | - | 1/1 = 100% | 2 |
| 2 | 3 | 3 | 2 | rand(2) | 1/2 = 50% | 2或3 |
| 3 | 4 | 3 | 3 | rand(3) | 1/3 = 33% | 2或3或4 |
代码实现
C# 实现
public class Solution {
// 方法1:哈希表预处理
private Dictionary<int, List<int>> indexMap;
private Random random;
public Solution(int[] nums) {
indexMap = new Dictionary<int, List<int>>();
random = new Random();
// 预处理:存储每个值对应的所有索引
for (int i = 0; i < nums.Length; i++) {
if (!indexMap.ContainsKey(nums[i])) {
indexMap[nums[i]] = new List<int>();
}
indexMap[nums[i]].Add(i);
}
}
public int Pick(int target) {
List<int> indices = indexMap[target];
return indices[random.Next(indices.Count)];
}
}
// 方法2:水塘抽样
public class SolutionReservoir {
private int[] nums;
private Random random;
public SolutionReservoir(int[] nums) {
this.nums = nums;
this.random = new Random();
}
public int Pick(int target) {
int count = 0;
int result = -1;
for (int i = 0; i < nums.Length; i++) {
if (nums[i] == target) {
count++;
// 以1/count的概率选择当前索引
if (random.Next(count) == 0) {
result = i;
}
}
}
return result;
}
}
Python 实现
import random
class Solution:
# 方法1:哈希表预处理
def __init__(self, nums: List[int]):
self.index_map = {}
# 预处理:存储每个值对应的所有索引
for i, num in enumerate(nums):
if num not in self.index_map:
self.index_map[num] = []
self.index_map[num].append(i)
def pick(self, target: int) -> int:
indices = self.index_map[target]
return random.choice(indices)
class SolutionReservoir:
# 方法2:水塘抽样
def __init__(self, nums: List[int]):
self.nums = nums
def pick(self, target: int) -> int:
count = 0
result = -1
for i, num in enumerate(self.nums):
if num == target:
count += 1
# 以1/count的概率选择当前索引
if random.randint(1, count) == 1:
result = i
return result
# 使用random.randrange的另一种实现
class SolutionReservoirV2:
def __init__(self, nums: List[int]):
self.nums = nums
def pick(self, target: int) -> int:
count = 0
result = -1
for i, num in enumerate(self.nums):
if num == target:
count += 1
# 以1/count的概率选择当前索引
if random.randrange(count) == 0:
result = i
return result
C++ 实现
class Solution {
private:
unordered_map<int, vector<int>> indexMap;
mt19937 gen;
public:
// 方法1:哈希表预处理
Solution(vector<int>& nums) : gen(random_device{}()) {
// 预处理:存储每个值对应的所有索引
for (int i = 0; i < nums.size(); i++) {
indexMap[nums[i]].push_back(i);
}
}
int pick(int target) {
vector<int>& indices = indexMap[target];
uniform_int_distribution<> dis(0, indices.size() - 1);
return indices[dis(gen)];
}
};
// 方法2:水塘抽样
class SolutionReservoir {
private:
vector<int> nums;
mt19937 gen;
public:
SolutionReservoir(vector<int>& nums) : nums(nums), gen(random_device{}()) {}
int pick(int target) {
int count = 0;
int result = -1;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == target) {
count++;
// 以1/count的概率选择当前索引
uniform_int_distribution<> dis(0, count - 1);
if (dis(gen) == 0) {
result = i;
}
}
}
return result;
}
};
执行结果
C# 实现
- 执行用时:147 ms(哈希表)/ 156 ms(水塘抽样)
- 内存消耗:52.8 MB(哈希表)/ 50.1 MB(水塘抽样)
Python 实现
- 执行用时:388 ms(哈希表)/ 421 ms(水塘抽样)
- 内存消耗:18.2 MB(哈希表)/ 17.8 MB(水塘抽样)
C++ 实现
- 执行用时:89 ms(哈希表)/ 101 ms(水塘抽样)
- 内存消耗:31.6 MB(哈希表)/ 30.8 MB(水塘抽样)
性能对比
| 解法 | 语言 | 执行用时 | 内存消耗 | 特点 |
|---|---|---|---|---|
| 哈希表 | C++ | 89 ms | 31.6 MB | pick函数O(1),适合频繁调用 |
| 水塘抽样 | C++ | 101 ms | 30.8 MB | 空间优化,适合内存受限场景 |
| 哈希表 | C# | 147 ms | 52.8 MB | 实现简单,性能稳定 |
| 水塘抽样 | Python | 421 ms | 17.8 MB | 内存最优,但执行较慢 |
代码亮点
- 🎯 算法选择灵活:根据不同场景选择哈希表或水塘抽样算法
- 💡 水塘抽样精髓:用O(1)空间实现等概率随机选择
- 🔍 随机数生成:使用现代随机数生成器保证随机性质量
- 🎨 空间时间权衡:提供两种截然不同的解决方案
常见错误分析
- 🚫 概率不均等:没有正确实现水塘抽样,导致选择概率不相等
- 🚫 随机数生成错误:使用错误的随机数范围或生成方式
- 🚫 边界条件处理:没有考虑target只出现一次的情况
- 🚫 内存溢出:在大数据场景下使用哈希表可能导致内存不足
解法对比
| 解法 | 构造时间 | pick时间 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 哈希表预处理 | O(n) | O(1) | O(n) | pick函数快速 | 内存占用大 |
| 水塘抽样 | O(1) | O(n) | O(1) | 内存效率高 | pick函数较慢 |
| 暴力每次遍历 | O(1) | O(n) | O(1) | 简单直观 | 无随机性保证 |
相关题目
- LeetCode 382. 链表随机节点 - 中等
- LeetCode 384. 打乱数组 - 中等
- LeetCode 528. 按权重随机选择 - 中等
- LeetCode 710. 黑名单中的随机数 - 困难
📖 系列导航
🔥 算法专题合集 - 查看完整合集
📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第398题。
💬 互动交流
感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。
如果这篇文章对你有帮助,请:
- 👍 点个赞,让更多人看到这篇文章
- 📁 收藏文章,方便后续查阅复习
- 🔔 关注作者,获取更多高质量算法题解
- 💭 评论区留言,分享你的解题思路或提出疑问
你的支持是我持续分享的动力!
💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!