一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情。
题目描述
实现 RandomizedSet 类:
RandomizedSet()初始化RandomizedSet对象bool insert(int val)当元素val不存在时,向集合中插入该项,并返回true;否则,返回false。bool remove(int val)当元素val存在时,从集合中移除该项,并返回true;否则,返回false。int getRandom()随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。 你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 。
提示:
- 最多调用
insert、remove和getRandom函数 次 - 在调用
getRandom方法时,数据结构中 至少存在一个 元素。
示例:
输入
["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"]
[[], [1], [2], [2], [], [1], [2], []]
输出
[null, true, false, true, 2, true, false, 2]
解释
RandomizedSet randomizedSet = new RandomizedSet();
randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。
randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。
randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。
randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。
randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。
randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。
randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。
整理题意: 按照题目要求完成实现 RandomizedSet 类的四个函数方法:RandomizedSet() 、bool insert(int val) 、bool remove(int val) 、 int getRandom()。
解题思路分析
题目要求实现一个类,满足插入、删除和获取随机元素操作的平均时间复杂度为 ,这道题难点在于对时间复杂度上的要求。
- 首先我们要在 时间内完成插入和删除操作,我们可以想到使用哈希表
unordered_map在 的时间内完成插入和删除操作,但是由于哈希表无法根据下标定位到特定元素,因此不能在 的时间内完成获取随机元素操作。 - 数组可以在 的时间内完成获取随机元素操作,支持尾部插入删除元素的变长数组
vector不仅能在 的时间内完成获取随机元素操作,还能在 的时间内完成尾部元素的插入和删除操作,但是由于无法在 的时间内判断元素是否存在,因此不能在 的时间内完成插入和删除操作。 - 为了满足插入、删除和获取随机元素操作的时间复杂度都是 ,需要将变长数组
vector和哈希表unordered_map结合,变长数组中存储元素,哈希表中存储每个元素在变长数组中的下标。 - 这样问题的关键就不是插入和随机获取元素了,而是删除,怎样做到 时间从
vector容器内删除指定元素呢?显然,要从vector容器内删除元素,只能从其尾部删除。所以我们可以先交换vector队尾元素和待删除元素的值(因为哈希表unordered_map中存储了下标,所以可以直接找到待删除元素的下标),然后把队尾元素删除,并更新原队尾元素的下标即可,其他位置的元素下标并没有变化。
复杂度分析
- 时间复杂度:初始化和各项操作的时间复杂度都是 。
- 空间复杂度:,其中 是集合中的元素个数。存储元素的数组和哈希表需要 的空间。
具体实现
bool insert(int val):插入操作时,首先判断val是否在哈希表unordered_map中,如果已经存在则返回false,如果不存在则插入val,操作如下:vector.push_back(val):在变长数组vector的末尾添加;- 注意变长数组
vector是从下标0开始存放的,在添加val之前的变长数组长度为val所在下标index,也就是添加val之后的变长数组长度减1,将val和下标index存入哈希表unordered_map[val] = index; - 返回
true。
bool remove(int val):删除操作时,首先判断val是否在哈希表中,如果不存在则返回false,如果存在则删除val,操作如下:index = unordered_map[val]:从哈希表unordered_map中获得val的下标index;- 将变长数组的最后一个元素
last移动到下标index处,在哈希表中将last的下标更新为index; vector.pop_back():在变长数组中删除最后一个元素;unordered_map.erase(val):在哈希表中删除val;- 返回
true。
int getRandom():获取随机元素操作时,由于变长数组中的所有元素的下标都连续,因此随机选取一个下标,返回变长数组中该下标处的元素,操作如下:nums[rand() % nums.size()]:rand() % nums.size()的范围为,左闭右开区间。
注意细节 初始化随机种子
srand((unsigned)time(NULL)):srand()函数是随机数发生器的初始化函数。原型:void srand(unsigned seed);如果使用相同的种子后面的rand()函数会出现一样的随机数,也就是所谓的 伪随机 。当srand()参数值固定的时候(默认自动调用srand(1),也就是以1为随机种子),rand()获得的数也是固定的。所以一般srand()的参数用(unsigned)time(NULL),因为系统的时间一直在变,所以rand()获得的数,也就一直在变,相当于是随机数了。
代码实现
class RandomizedSet {
private:
unordered_map<int, int> mp;
vector<int> nums;
public:
RandomizedSet() {
//使用系统时间来初始化随机种子
srand((unsigned)time(NULL));
//初始化哈希表和变长数组
mp.clear();
nums.clear();
}
bool insert(int val) {
//如果val元素存在
if(mp.count(val)) return false;
//添加val之前的变长数组长度为val所在下标index
mp[val] = nums.size();
nums.push_back(val);
return true;
}
bool remove(int val) {
if(mp.count(val) == 0) return false;
//n表示最后一个元素的下标
int n = nums.size() - 1;
//将mp[val]表示val在nums数组中的下标位置,将其替换为最后一个元素
nums[mp[val]] = nums[n];
//更新mp
mp[nums[n]] = mp[val];
//将val从mp中删除
mp.erase(val);
//将最后一个元素弹出
nums.pop_back();
return true;
}
int getRandom() {
//rand() % nums.size()的范围为[0, nums.size()),注意左闭右开区间
return nums[rand() % nums.size()];
}
};
/**
* Your RandomizedSet object will be instantiated and called as such:
* RandomizedSet* obj = new RandomizedSet();
* bool param_1 = obj->insert(val);
* bool param_2 = obj->remove(val);
* int param_3 = obj->getRandom();
*/
总结
本题的 核心思想 是将变长数组和哈希表结合。重点在于删除操作的手法(将变长数组的最后一个元素移动到待删除元素的下标处,然后删除变长数组的最后一个元素)。该操作手法的时间复杂度是 ,且可以保证在删除操作之后变长数组中的所有元素的下标都连续,方便插入操作和获取随机元素操作。此外运用到了随机函数 rand()(伪随机) 和初始化随机种子函数 srand(),并且设置初始化随机种子参数为 (unsigned)time(NULL) ,保证了随机数随着系统时间的改变而改变,相当于是随机数了。
结束语
人之所以要努力,就是为了把命运掌握在自己手里。没有谁可以一直被你依赖,也没有人能够替你成长,靠自己的力量一步步跨过泥泞,才能离想要的生活越来越近。