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 - 1
  • targetnums 中的一个整数
  • 最多调用 pick 函数 10^4

题目链接LeetCode 398. 随机数索引

解题思路

这道题有两种主要的解题思路:

方法一:预处理 + 随机选择

在构造函数中预处理数组,将每个值对应的所有索引存储在哈希表中,然后在pick函数中随机选择一个索引。

方法二:水塘抽样算法(Reservoir Sampling)

每次调用pick函数时遍历数组,使用水塘抽样算法保证每个满足条件的索引都有相等的被选中概率。

算法原理

方法一:哈希表预处理

思路

  1. 在构造函数中遍历数组,将每个值对应的所有索引存储在哈希表中
  2. pick函数中,根据target找到对应的索引列表,随机选择一个返回

优势pick函数的时间复杂度为O(1) 劣势:需要O(n)的额外空间存储所有索引

方法二:水塘抽样算法

核心思想: 对于数组中第i个等于target的元素,我们以1/i的概率选择它。

算法步骤

  1. 初始化count = 0,result = -1
  2. 遍历数组,当遇到target时:
    • count++
    • 以1/count的概率更新result为当前索引
  3. 返回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 内存最优,但执行较慢

代码亮点

  1. 🎯 算法选择灵活:根据不同场景选择哈希表或水塘抽样算法
  2. 💡 水塘抽样精髓:用O(1)空间实现等概率随机选择
  3. 🔍 随机数生成:使用现代随机数生成器保证随机性质量
  4. 🎨 空间时间权衡:提供两种截然不同的解决方案

常见错误分析

  1. 🚫 概率不均等:没有正确实现水塘抽样,导致选择概率不相等
  2. 🚫 随机数生成错误:使用错误的随机数范围或生成方式
  3. 🚫 边界条件处理:没有考虑target只出现一次的情况
  4. 🚫 内存溢出:在大数据场景下使用哈希表可能导致内存不足

解法对比

解法 构造时间 pick时间 空间复杂度 优点 缺点
哈希表预处理 O(n) O(1) O(n) pick函数快速 内存占用大
水塘抽样 O(1) O(n) O(1) 内存效率高 pick函数较慢
暴力每次遍历 O(1) O(n) O(1) 简单直观 无随机性保证

相关题目


📖 系列导航

🔥 算法专题合集 - 查看完整合集

📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第398题。


💬 互动交流

感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。

如果这篇文章对你有帮助,请:

  • 👍 点个赞,让更多人看到这篇文章
  • 📁 收藏文章,方便后续查阅复习
  • 🔔 关注作者,获取更多高质量算法题解
  • 💭 评论区留言,分享你的解题思路或提出疑问

你的支持是我持续分享的动力!

💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!