LeetCode 398. 随机数索引(哈希表/水塘抽样)

本文探讨了在Java中使用哈希表和水塘抽样算法解决398号问题——随机数索引。哈希表方法用于存储重复元素及其下标,而水塘抽样则适用于大数据流中的概率均匀选取。两种方法的时间和空间复杂度分析以及代码实现详尽解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


题目

在这里插入图片描述

方法一:哈希表

在构造函数中,使用哈希表记录nums中相同元素的下标

pick函数中,从哈希表中取出target对应的下标列表,然后随机选择其中一个下标并返回

class Solution {
    Map<Integer, List<Integer>> map = new HashMap<>();
    public Solution(int[] nums) {
        for (int i = 0; i < nums.length; i++) {
            if (map.containsKey(nums[i])) {
                map.get(nums[i]).add(i);
            } else {
                List<Integer> list = new ArrayList<>();
                list.add(i);
                map.put(nums[i], list);
            }
        }        
    }
    
    public int pick(int target) {
        Random random = new Random();
        int index = random.nextInt(map.get(target).size());
        return map.get(target).get(index);
    }
}

/**
 * Your Solution object will be instantiated and called as such:
 * Solution obj = new Solution(nums);
 * int param_1 = obj.pick(target);
 */
  • 时间复杂度:初始化为 O ( n ) O(n) O(n)pick O ( 1 ) O(1) O(1),其中 n n nnums 的长度

  • 空间复杂度: O ( n ) O(n) O(n)。需要 O ( n ) O(n) O(n) 的空间存储 n n n 个下标

方法二:水塘抽样(不定长数据流)

大数据流中的随机抽样问题,即:当内存无法加载全部数据时,如何从包含未知大小的数据流中随机选取k个数据,并且要保证每个数据被抽取到的概率相等

k = 1 k = 1 k=1时:

假设数据流含有 N N N个数,如果要保证所有的数被抽到的概率相等,那么每个数抽到的概率应该为 1 N \frac{1}{N} N1.

可以这样做:

  • 遇到第 1 1 1个数 n 1 n_1 n1 的时候,保留它, p ( n 1 ) = 1 p(n_1)=1 p(n1)=1
  • 遇到第 2 2 2个数 n 2 n_2 n2 的时候,以 1 2 \frac{1}{2} 21 的概率保留它,那么 p ( n 1 ) = 1 × 1 2 = 1 2 p(n_1)=1 \times \frac{1}{2}=\frac{1}{2} p(n1)=1×21=21 p ( n 2 ) = 1 2 p(n_2)=\frac{1}{2} p(n2)=21
  • 遇到第 3 3 3个数 n 3 n_3 n3 的时候,以 1 3 \frac{1}{3} 31 的概率保留它,那么 p ( n 1 ) = p ( n 2 ) = 1 2 × ( 1 − 1 3 ) = 1 3 p(n_1)=p(n_2)=\frac{1}{2}\times (1-\frac{1}{3})=\frac{1}{3} p(n1)=p(n2)=21×(131)=31 p ( n 3 ) = 1 3 p(n_3)=\frac{1}{3} p(n3)=31
  • ⋯ \cdots
  • 遇到第 i i i个数 n i n_i ni 的时候,以 1 i \frac{1}{i} i1 的概率保留它,那么 p ( n 1 ) = p ( n 2 ) = p ( n 3 ) = ⋯ = p ( n i − 1 ) = 1 i − 1 × ( 1 − 1 i ) = 1 i p(n_1)=p(n_2)=p(n_3)=\cdots=p(n_{i-1})=\frac{1}{i-1}\times (1 - \frac{1}{i})=\frac{1}{i} p(n1)=p(n2)=p(n3)==p(ni1)=i11×(1i1)=i1 p ( n i ) = 1 i p(n_i)=\frac{1}{i} p(ni)=i1

可以看出,对于 k = 1 k=1 k=1的情况,可以制定这样简单的抽样策略:

数据流中第 i i i个数被保留的概率为 1 i \frac{1}{i} i1 。只要采取这种策略,只需要遍历一遍数据流就可以得到采样值,并且保证所有数被选取的概率均为 1 N \frac{1}{N} N1

k > 1 k >1 k>1时:

仍然假设数据流中含有 N N N个数,那么要保证所有的数被抽到的概率相等,每个数被选取的概率必然为 k N \frac{k}{N} Nk

  • 对于前 k k k个数 n 1 , n 2 , ⋯   , n k n_1,n_2,\cdots,n_k n1,n2,,nk,保留下来,则 p ( n 1 ) = p ( n 2 ) = ⋯ = p ( n k ) = 1 p(n_1)=p(n_2)=\cdots=p(n_k)=1 p(n1)=p(n2)==p(nk)=1(下面连等采用 p ( n 1 : k ) p(n_{1:k}) p(n1:k) 的形式)
  • 对于第$k+1
    $个数 n k + 1 n_{k+1} nk+1,以 k k + 1 \frac{k}{k+1} k+1k 的概率保留它(这里只是指本次被保留下来),那么前 k k k个数中的 n r ( r ∈ 1 : k ) n_r(r\in 1:k) nr(r1:k)被保留的概率可以这样表示: p ( n r 被 保 留 ) = p ( 上 一 轮 n r 被 保 留 ) × ( p ( n k + 1 被 丢 弃 ) + p ( n k + 1 被 保 留 ) × p ( n r 未 被 替 换 ) ) p(n_r被保留)=p(上一轮n_r被保留)\times(p(n_{k+1}被丢弃)+p(n_{k+1}被保留)\times p(n_r未被替换)) p(nr)=p(nr)×(p(nk+1)+p(nk+1)×p(nr)),即 p ( n 1 : k ) = 1 k + 1 + k k + 1 × k − 1 k = k k + 1 p(n_{1:k})=\frac{1}{k+1}+\frac{k}{k+1}\times \frac{k-1}{k}=\frac{k}{k+1} p(n1:k)=k+11+k+1k×kk1=k+1k
  • 对于第 k + 2 k+2 k+2个数 n k + 2 n_{k+2} nk+2,以 k k + 2 \frac{k}{k+2} k+2k 的概率保留它(这里只是指本次被保留下来),那么前 k k k个被保留下来的数中的 n r ( r ∈ 1 : k ) n_r(r\in 1:k) nr(r1:k)被保留的概率为 p ( n 1 : k ) = k k + 1 × ( 2 k + 2 + k k + 2 × k − 1 k ) = k k + 2 p(n_{1:k})=\frac{k}{k+1}\times (\frac{2}{k+2}+\frac{k}{k+2}\times \frac{k-1}{k})=\frac{k}{k+2} p(n1:k)=k+1k×(k+22+k+2k×kk1)=k+2k
  • ⋯ \cdots
  • 对于第 i ( i > k ) i(i>k) ii>k个数 n i n_{i} ni ,以 k i \frac{k}{i} ik 的概率保留它,前 i − 1 i-1 i1个数中的 n r ( r ∈ 1 : i − 1 ) n_r(r\in 1:i-1) nr(r1:i1) 被保留的概率为 p ( n 1 : k ) = k i − 1 × ( i − k i + k i × k − 1 k ) = k i p(n_{1:k})=\frac{k}{i-1}\times (\frac{i-k}{i}+\frac{k}{i}\times\frac{k-1}{k})=\frac{k}{i} p(n1:k)=i1k×(iik+ik×kk1)=ik

这样,我们可以制订策略:

对于前 k k k个数,全部保留,对于第 i ( i > k ) i(i>k) ii>k个数,以 k i \frac{k}{i} ik 的概率保留第 i i i个数,并以 1 k \frac{1}{k} k1 的概率与前面已选择的 k k k个数中的任意一个替换。

class Solution {
    Random random = new Random();
    int[] nums;
    public Solution(int[] _nums) {
        nums = _nums;
    }
    public int pick(int target) {
        int n = nums.length, ans = 0;
        for (int i = 0, cnt = 0; i < n; i++) {
            if (nums[i] == target) {
                cnt++;
                if (random.nextInt(cnt) == 0) ans = i;
            }
        }
        return ans;
    }
}
  • 时间复杂度:初始化的复杂度为 O ( 1 ) O(1) O(1)pick 操作的复杂度为 O ( n ) O(n) O(n)
  • 空间复杂度: O ( n ) O(n) O(n)

Reference

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xylitolz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值