目录

LC 398. 随机数索引 (opens new window) (opens new window)

中等

# 问题描述

给你一个可能含有 重复元素 的整数数组 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 * 104
  • -231 <= nums[i] <= 231 - 1
  • targetnums 中的一个整数
  • 最多调用 pick 函数 104

# 哈希表预处理

一般情况下,我们首先想到的是使用哈希表对数组进行预处理,在构造函数传入 numsnums 时,遍历 numsnums 并存储每个 nums[i]nums[i] 对应的下标集合,即使用哈希表以 nums[i]nums[i] 为键,下标集合 ListList 作为值进行存储。

pickpick 操作时,通过 O(1)O(1) 的复杂度取出所有 nums[i]=targetnums[i] = target 的集合下标,再随机一个下标进行返回。

实现代码:

/**
 * @param {number[]} nums
 */
var Solution = function (nums) {
  this.map = new Map()
  const n = nums.length
  for (let i = 0; i < n; i++) {
    let list
    if (this.map.has(nums[i])) {
      list = this.map.get(nums[i])
    } else {
      list = []
    }
    list.push(i)
    this.map.set(nums[i], list)
  }
}

/**
 * @param {number} target
 * @return {number}
 */
Solution.prototype.pick = function (target) {
  const list = this.map.get(target)
  return list[~~(Math.random() * list.length)]
}

/**
 * Your Solution object will be instantiated and called as such:
 * var obj = new Solution(nums)
 * var param_1 = obj.pick(target)
 */
  • 时间复杂度:初始化的复杂度为 O(n)O(n)pickpick 操作的复杂度为 O(1)O(1)
  • 空间复杂度:O(n)O(n)

# 蓄水池抽样

如果数据流是不定长或者很大,我们无法使用诸如数组的容器存储所有满足条件的下标,哈希表预处理的方法就无法满足了,此时我们可以使用蓄水池抽样算法来解决该问题。

我们在每次 pickpick 操作时,对数据流进行遍历,当我们第 kk 次遇到满足 nums[i]=targetnums[i] = target 的数据时,我们都执行一次判断,确定「是否要将当前 i 作为最新答案候选」。具体的,我们随机生成 [0,k)[0, k) 中的随机数,若生成的数为 00 (概率为1k\tfrac{1}{k}),则将当前 ii 作为最新的候选答案。最终数据流遍历完后,假设我们共有 mm 个数据满足 nums[i]=targetnums[i] = target ,每一位下标返回的概率均为1m\tfrac{1}{m}

证明该方法的正确性,假设我们最终得到的答案为 kk ,那么 kk 成为答案的充要条件为 「遍历到 k 位置时, k 选为最新候选答案」 且 「遍历到大于 k 位置时, 均不更新最新候选答案」。该事件发生的概率为:

P=1k(11k+1)(11k+2)(11n)P = \tfrac{1}{k} * (1 - \tfrac{1}{k+1}) * (1 - \tfrac{1}{k+2}) * \dots * (1 - \tfrac{1}{n})

首项为 kk 选为最新候选答案的概率,后面的每一项代表 kk 不被更新的概率。化简得到:

P=1kkk+1k+1k+2n1nP = \tfrac{1}{k} * \tfrac{k}{k+1} * \tfrac{k+1}{k+2} * \dots * \tfrac{n-1}{n}

进一步简化可得:

P=1nP = \tfrac{1}{n}

实现代码:

/**
 * @param {number[]} nums
 */
var Solution = function (nums) {
  this.nums = nums
}

/**
 * @param {number} target
 * @return {number}
 */
Solution.prototype.pick = function (target) {
  let count = 0
  let ans = 0
  for (let i = 0; i < this.nums.length; i++) {
    if (this.nums[i] === target) {
      ans = ~~(Math.random() * ++count) === 0 ? i : ans
    }
  }
  return ans
}

/**
 * Your Solution object will be instantiated and called as such:
 * var obj = new Solution(nums)
 * var param_1 = obj.pick(target)
 */
  • 时间复杂度:初始化的复杂度为 O(1)O(1)pickpick 操作的复杂度为 O(n)O(n)
  • 空间复杂度:O(n)O(n)

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 , 转载请注明出处!