【C/C++】380. O(1) 时间插入、删除和获取随机元素

389 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情


题目链接:380. O(1) 时间插入、删除和获取随机元素

题目描述

实现 RandomizedSet 类:

  • RandomizedSet() 初始化 RandomizedSet 对象
  • bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false
  • bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false
  • int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。 你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1)O(1)

提示:

  • 231val2311-2^{31} \leqslant val \leqslant 2^{31} - 1
  • 最多调用 insertremovegetRandom 函数 21052*10^5
  • 在调用 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()

解题思路分析

题目要求实现一个类,满足插入、删除和获取随机元素操作的平均时间复杂度为 O(1)O(1),这道题难点在于对时间复杂度上的要求。

  1. 首先我们要在 O(1)O(1) 时间内完成插入和删除操作,我们可以想到使用哈希表 unordered_mapO(1)O(1) 的时间内完成插入和删除操作,但是由于哈希表无法根据下标定位到特定元素,因此不能在 O(1)O(1) 的时间内完成获取随机元素操作。
  2. 数组可以在 O(1)O(1) 的时间内完成获取随机元素操作,支持尾部插入删除元素的变长数组 vector 不仅能在 O(1)O(1) 的时间内完成获取随机元素操作,还能在 O(1)O(1) 的时间内完成尾部元素的插入和删除操作,但是由于无法在 O(1)O(1) 的时间内判断元素是否存在,因此不能在 O(1)O(1) 的时间内完成插入和删除操作。
  3. 为了满足插入、删除和获取随机元素操作的时间复杂度都是 O(1)O(1),需要将变长数组 vector 和哈希表 unordered_map 结合,变长数组中存储元素,哈希表中存储每个元素在变长数组中的下标。
  4. 这样问题的关键就不是插入和随机获取元素了,而是删除,怎样做到 O(1)O(1) 时间从 vector 容器内删除指定元素呢?显然,要从 vector 容器内删除元素,只能从其尾部删除。所以我们可以先交换 vector 队尾元素和待删除元素的值(因为哈希表 unordered_map 中存储了下标,所以可以直接找到待删除元素的下标),然后把队尾元素删除,并更新原队尾元素的下标即可,其他位置的元素下标并没有变化。

复杂度分析

  • 时间复杂度:初始化和各项操作的时间复杂度都是 O(1)O(1)
  • 空间复杂度:O(n)O(n),其中 nn 是集合中的元素个数。存储元素的数组和哈希表需要 O(n)O(n) 的空间。

具体实现

  1. 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
  2. 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
  3. int getRandom():获取随机元素操作时,由于变长数组中的所有元素的下标都连续,因此随机选取一个下标,返回变长数组中该下标处的元素,操作如下:
    • nums[rand() % nums.size()]rand() % nums.size() 的范围为[0,nums.size())[0, 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();
 */

总结

本题的 核心思想 是将变长数组和哈希表结合。重点在于删除操作的手法(将变长数组的最后一个元素移动到待删除元素的下标处,然后删除变长数组的最后一个元素)。该操作手法的时间复杂度是 O(1)O(1),且可以保证在删除操作之后变长数组中的所有元素的下标都连续,方便插入操作和获取随机元素操作。此外运用到了随机函数 rand()(伪随机) 和初始化随机种子函数 srand(),并且设置初始化随机种子参数为 (unsigned)time(NULL) ,保证了随机数随着系统时间的改变而改变,相当于是随机数了。


结束语

人之所以要努力,就是为了把命运掌握在自己手里。没有谁可以一直被你依赖,也没有人能够替你成长,靠自己的力量一步步跨过泥泞,才能离想要的生活越来越近。