原题链接:【398. 随机数索引】:https://leetcode-cn.com/problems/random-pick-index/
题目描述:
给定一个可能含有重复元素的整数数组,要求随机输出给定的数字的索引。 您可以假设给定的数字一定存在于数组中。
注意:
数组大小可能非常大。 使用太多额外空间的解决方案将不会通过测试。
示例:
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);
解题思路:
解法1:哈希表。
暴力解法。使用哈希表存储数组中每个元素值到其所有的下标索引的映射。
即key为元素值,value为元素值对应的索引下标列表。
再用random函数,从下标列表中随机选择一个索引返回即可。
这个解法非常占用额外空间O(N),如果数组很大,很容易oom。
解法2:动态数组存储索引列表。
定义一个存储索引下标的索引列表indexList,用动态数组,方便扩展。
遍历数组,如果元素跟target目标值相等,则把索引值添加进indexList列表。
取随机数,rand范围为【0,indexList.length ),然后将对应位置的索引值返回即可。
概率为:1 / indexList.length
这里相比上一种解法,已经降了一个数量级,但是如果N很大,或者数字分布不均匀,indexList也可能非常大,也非常占用额外空间,空间消耗也可能会达到O(N),可能会oom。
解法3:蓄水池抽样算法。
算法是:要从超大数组N中取k个元素,这里是取一个元素,即K=1
那么每次只取一个样本值,那么逐个遍历数组,当遇到第i个数时,就以 1 / i 的概率抽取它做样本值,以(i - 1)/ i 的概率保留原来的样本值,不替换。
我们不需要保存索引下标列表,只需要一个变量cnt计数,统计匹配到了多少个相同的数字,方便后去取随机数,算概率。
遍历数组,如果元素跟target目标值相等,则cnt++,计数器+1。
然后取随机数,rand范围为【1,cnt】。
因为这里只取一个数,就是K=1的案例。
所以如果rand == 1,则取当前元素的下标,替换为结果值。
如果rand > 1,则保留之前的结果,继续往下走。知道结束返回结果值。
特点:这里只需要遍历一遍数组,而且没有额外的空间消耗,只有一个计数器变量cnt。空间复杂度为O(1)。(非常推荐)
题解代码:
代码如下:(java版)
//给定一个可能含有重复元素的整数数组,要求随机输出给定的数字的索引。 您可以假设给定的数字一定存在于数组中。 // // 注意: //数组大小可能非常大。 使用太多额外空间的解决方案将不会通过测试。 // // 示例: // // //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); // // Related Topics 水塘抽样 哈希表 数学 随机化 // 125 0 import java.util.Random; //leetcode submit region begin(Prohibit modification and deletion) class Solution { // 需要把nums传递给pick方法,所以我们需要定义一个类变量 int[] nums; public Solution(int[] nums) { this.nums = nums; } public int pick(int target) { // 定义一个变量,存储结果值 int result = 0; // 定义一个变量cnt,计数器,标记匹配到了几个相同的数字,方便后面取随机数范围 int cnt = 0; // 循环遍历链表 for (int i=0; i