随机数索引(一题双解)【Leetcode每日(4.25)一题】C++
前言
今天的每日一题的质量蛮高的,我们可以有时间优先和空间优先的两种角度去思考解题并优化算法和代码。
题目描述
给定一个可能含有重复元素的整数数组,要求随机输出给定的数字的索引。 您可以假设给定的数字一定存在于数组中。
注意:
数组大小可能非常大。 使用太多额外空间的解决方案将不会通过测试。
示例
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);
以下是题解:
哈希表——空间换时间
首先我们不难想出用空间换时间的思路,使用键值对
比如哈希表的数据结构,以数值为键,下标为值存储。而值可以采用可变数组vector存储。
所以我们可以在创建类对象的时候建立哈希表,然后在pick函数中随机返回值对应的下标。
这样的好处在于,置需要遍历一遍数组,将数组存入哈希表,然调用pick函数直接返回即可,时间复杂度O(n), 空间复杂度O(n)。
以下是C++
代码
Code(C++)
#include<iostream>
#include<unordered_map>
#include<vector>
using namespace std;
class Solution
{
private:
// 一个哈希表, 用来存储相同值对应的下标索引
unordered_map<int, vector<int>> indexs;
public:
/// <summary>
/// 在类对象构造时遍历实现下标哈希表的建立
/// </summary>
/// <param name="nums"></param>
Solution(vector<int>& nums)
{
for (int i = 0; i < nums.size(); ++i)
{
// 存储同值下标
indexs[nums[i]].push_back(i);
}
}
/// <summary>
/// 调用哈希表和随机函数来获取哈希表
/// </summary>
/// <param name="target"></param>
/// <returns></returns>
int pick(int target)
{
int size = indexs[target].size();
vector<int>& index = indexs[target];
// 返回随机索引
return index[rand() % size];
}
};
/**
* Your Solution object will be instantiated and called as such:
* Solution* obj = new Solution(nums);
* int param_1 = obj->pick(target);
*/
int main(int argc, char** argv)
{
int n;
while (cin >> n)
{
vector<int> nums(n);
for (int& num : nums)
{
cin >> num;
}
Solution* obj = new Solution(nums);
int op; // 命令参数,0——结束,1——继续,即pick指令
cin >> op;
while (op > 0)
{
int target;
cin >> target;
int param_1 = obj->pick(target); // 调用pick函数,得到随机下标索引
cout << param_1 << endl;
cin >> op;
}
}
return 0;
}
概率论,不放回抽样——时间换空间
欸,如果我们从空间优先的角度出发,会怎么样呢?
我们不难发现,如果不使用额外的空间,那我们在调用pick函数的时候,显然至少需要遍历数组的,而且我们需要在找到数值时,随机判断是否返回。
欸?这个模式,大家是否有点熟悉,没错,这就是我们在概率论常遇到不放回抽样问题,不妨假设数组中要找到的值有n个,那么对于找到的第一个的下标,其概率有
1
n
对
于
第
二
个
有
:
n
−
1
n
∗
1
n
−
1
=
1
n
\frac{1}{n} \\ 对于第二个有:\frac{n - 1}{n} * \frac{1}{n - 1} = \frac{1}{n}
n1对于第二个有:nn−1∗n−11=n1
以此类推,不难发现每一个下标返回的概率都是 1/ n。
所以我们可以在每次pick函数调用中,先遍历数组,如果找到元素时,就 rand()%n
, 如果得到0,欸,问题又来了,在遍历的时候,我们又不知道有多少个同值数组。其实也很简单,我们可以在遍历的时候记录数量,我们只需要考虑当前的数量然后得到随机值,我们置需要记录最后一次随机为0的下标即可,比如
第
一
次
:
1
∗
1
2
∗
.
.
.
∗
n
−
1
n
=
1
/
n
第
k
次
:
我
们
不
用
管
前
面
的
,
只
需
从
当
前
开
始
考
虑
1
k
∗
k
k
+
1
∗
.
.
.
n
−
1
n
=
1
n
第一次:1 * \frac{1}{2} * ... * \frac{n -1}{n} = 1/n \\ 第k次: 我们不用管前面的,只需从当前开始考虑\\ \frac {1}{k} * \frac {k}{k+1} * ... \frac{n-1}{n} = \frac{1}{n}
第一次:1∗21∗...∗nn−1=1/n第k次:我们不用管前面的,只需从当前开始考虑k1∗k+1k∗...nn−1=n1
思路清晰了,我们就来上代码吧
Code(C++)
#include<iostream>
#include<unordered_map>
#include<vector>
using namespace std;
class Solution
{
private:
vector<int>& nums;
public:
/// <summary>
/// 引用对象
/// </summary>
/// <param name="nums"></param>
Solution(vector<int>& nums) : nums(nums) // 引用对象,其实不占用空间
{}
/// <summary>
/// 遍历数组
/// </summary>
/// <param name="target"></param>
/// <returns></returns>
int pick(int target)
{
int cnt = 0; // 当前找到的target的数量
int ans = -1; // 初始化答案
// 遍历数组
for (int i = 0; i < nums.size(); ++i)
{
if (nums[i] == target) // 找到数值
{
cnt++; // 数量+1
if (rand() % cnt == 0) // 更新答案
{
ans = i;
}
}
}
return ans;
}
};
/**
* Your Solution object will be instantiated and called as such:
* Solution* obj = new Solution(nums);
* int param_1 = obj->pick(target);
*/
int main(int argc, char** argv)
{
int n;
while (cin >> n)
{
vector<int> nums(n);
for (int& num : nums)
{
cin >> num;
}
Solution* obj = new Solution(nums);
int op; // 命令参数,0——结束,1——继续,即pick指令
cin >> op;
while (op > 0)
{
int target;
cin >> target;
int param_1 = obj->pick(target); // 调用pick函数,得到随机下标索引
cout << param_1 << endl;
cin >> op;
}
}
return 0;
}
后话
继续加油!