给定一个可能含有重复元素的整数数组,要求随机输出给定的数字的索引。 您可以假设给定的数字一定存在于数组中。
注意:
数组大小可能非常大。 使用太多额外空间的解决方案将不会通过测试。示例:
int[] nums = new int[] {1,2,3,3,3}; Solution solution = new Solution(nums); // pick(3) 应该返回索引 2,3 或者 4。每个索引的返回概率应该相等。 solution.pick(3); // pick(1) 应该返回 0。因为只有nums[0]等于1。 solution.pick(1);
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/random-pick-index
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
基本思想
第一反应是使用哈希表存储所有出现数字及对应的数字下标,需要查找时,直接按照target数字查哈希表,对于所有可能的下标随机选一个即可,即使用rand() % n的方式就能做到均匀随机访问,构造需要的时间对应的复杂度为,访问的时间复杂度为
,但是空间复杂度为
,这显然不合题意中使用较少额外空间的题意。
备用方法是使用水塘抽样的思路,这种方法是一种线上解法,也即扫描过边扫描数组边更新返回结果,但是官解似乎给的还是先把数字拷贝到Solution类对象中,构造数组所花的时间复杂度为(?),可能本意应该是每次pick都根据之前的nums数组来?
下面介绍使用水塘抽样的原理:
水塘抽样简介:
水塘抽样(Reservoir Sampling) - 知乎 (zhihu.com)
主要思想就是“以时间换空间”,在文件大到无法一次装入内存时,仍然可以等概率的获取相同值的变量。
具体描述
前置原理:
假设该文件中存在数据流,该数据流中所有整型变量的个数为N,我们需要的target值大小的整型变量可能有若干个,假设有n个,为了保障能等概率地所有选到的数,首先容易想到:所有可能发生事件的一个划分为:target_{i}为选中第i个值为target的变量的事件,加overline的则为没选中第i个值为target的变量的事件。
选中第n个target的概率为1/n,这里可以直接rand() % n,前面n-1个数字选到与否都无关最后结果,因为被第n个target选择到的结果“污染”了,所以对n而言,选择到的概率为:
(1)
选中第n-1个target的概率也应该为1/n,这里对应的随机事件为:①选中第n-1个target并且,②没有选中第n个target。①②两个事件相互独立,所以对应的概率描述如下:
(2)
而根据前式(1)可得:
以此类推,可以不断得到对于第i个target选中的事件如下(第i次选中,后边所有出现的target均不选取):
那么该如何实现呢?可以使用cnt变量存储访问到target的次数,每次访问到target,先cnt++,然后选中第cnt位target的概率要为1/cnt,则只需rand() % cnt即可,过程还是比较简单的。
实现代码(C++)
class Solution {
private:
vector<int> nums;
public:
Solution(vector<int>& nums) {
this->nums = nums;
}
int pick(int target) {
int res;
int cnt = 0;
for(int i = 0; i < nums.size(); i++) {
if(target == nums[i]) {
cnt++;
if(rand() % cnt == 0) {
res = i;
}
}
}
return res;
}
};
/**
* Your Solution object will be instantiated and called as such:
* Solution* obj = new Solution(nums);
* int param_1 = obj->pick(target);
*/
最终结果