【小白爬Leetcode381】O(1) 时间插入、删除和获取随机元素 - 允许重复 Convert BST to Greater Tree

@[TOC](【小白爬Leetcode381】O(1) 时间插入、删除和获取随机元素 - 允许重复 Convert BST to Greater Tree)

题目

Leetcode381 h a r d \color{#ff0000}{hard} hard
点击进入原题链接:O(1) 时间插入、删除和获取随机元素 - 允许重复

第一想法:

当时第一想法是:O(1)的时间插入、删除,这不就是哈希表的典型应用嘛!至于这个random…生成随机数?
于是有了下面这种解法:

  1. 维护一个哈希表,hashKey用插入的数字计算,value则代表这个数字出现了几次
  2. 维护一个Solution类的成员变量int num,表示一共有多少个数字,每插入一个新的数this->num++,每删除一个存在的数字,this->num--
  3. 返回随机数的时候,int randomValue = rand()%(this->num)+1;,得到一个1~this->num的随机数,然后遍历哈希表,每遍历一个就从randomValue里减去哈希表的值(也就是这个数字出现了几次),当randomValue<=0时,就返回当前遍历到的哈希表的key
    空间复杂度:O(N) 取决于插入数据的多少
    时间复杂度:
    【插入/删除】:O(1)
    【返回随机数】:O(n),最好情况下随机数是0,直接返回遍历哈希表得到的第一个数字,最坏情况下随机数是this->num,需要遍历整个哈希表。取最坏时间复杂度O(n)
class RandomizedCollection {
public:
    /** Initialize your data structure here. */
    RandomizedCollection() {
        this->num = 0;
    }
    
    /** Inserts a value to the collection. Returns true if the collection did not already contain the specified element. */
    bool insert(int val) {
        this->num++;
        bool res;
        if(this->M.find(val)==this->M.end()||this->M[val]==0){
            this->M[val] = 1;
            res = true;//不存在,true
        }else{
            this->M[val]++;
            res = false;//已经存在,false
        }
        return res;
    }
    
    /** Removes a value from the collection. Returns true if the collection contained the specified element. */
    bool remove(int val) {
        if(this->M.find(val)==this->M.end()||this->M[val]==0){
            return false;
        }else{
            M[val]--;
            this->num--;
            return true;
        }
    }
    
    /** Get a random element from the collection. */
    int getRandom() {
        if(this->num>0){
            srand((unsigned)time(NULL)); //这样设置srand是错的,每一次调用getRandom都用当前时间设置随机数种子,如果连续调用(相差时间不足1ms)得到的随机数是一样的
            int randomValue = rand()%(this->num)+1;
            for(auto it:M){
                randomValue -= it.second;
                if(randomValue<=0) return it.first;
            }
        }
        return 0;
    }
private:
    unordered_map <int,int> M;
    int num;
};

结果死在了最后一个测试用例:
在这里插入图片描述
按道理最后还剩1 10 100,这三个数字应该都应该被输出,结果最后输出的全是一样的值:
在这里插入图片描述
错误的原因在这里:
在getRandom函数里用当前时间设置srand,也就是说,每一次调用getRandom都会用当前系统时间设置随机数种子,如果连续调用(相差时间不足1ms)得到的随机数是一样的。所以会出现上面图的情况。

    int getRandom() {
        if(this->num>0){
            srand((unsigned)time(NULL)); //这样设置srand是错的,每一次调用getRandom都用当前时间设置随机数种子,如果连续调用(相差时间不足1ms)得到的随机数是一样的
            int randomValue = rand()%(this->num)+1;
            for(auto it:M){
                randomValue -= it.second;
                if(randomValue<=0) return it.first;
            }
        }
        return 0;
    }

其实这里根本不需要srand,因为这个测试用例也就跑一遍,也不需要每次返回的结果都不一样。但是在实际工程中,肯定要在main函数里设置好srand。

数组+哈希表

官方给出的解答:
首先在Solution类里维护一个数组V和一个哈希表M。数组用来存放数字,哈希表用来统计每个数字在数组中的所有位置

  1. 插入元素的时候,直接向数组V数组尾插val,并向哈希表中M[val]所映射的集合(unordered_set)中插入当前数组尾部的索引V.size()-1。说人话就是数组尾部插入数字,并在哈希表里记录这个数字在尾部的位置。
  2. 删除元素的时候,首先通过哈希表M获取当前元素在数组V中的位置。获取val在数组V中的位置,由于unordered_set是无序的,这里随便取集合的头部元素*(idx[val].begin)就行。
    获取到位置之后,肯定不能直接在数组V中删除该位置的元素,这样会导致数组后面的元素集体往前挪一位,肯定达不到O(1)的时间复杂度。因此采用当前位置的数字和数组尾部数字互换,然后把数组尾删一个元素即可。注意,这里的难点在于交换尾删的同时要更新哈希表M,被删除的数字在哈希表中要删除当前位置索引,最后一个数字在哈希表中要删去原来最后一个位置索引,并加上新的索引。
    这里有一个特殊情况,即如果要删除的数字本来就在数组的最后一个位置,那么同一个位置会被删除两遍,好在unordered_seterase方法可以处理删除的元素不存在的情况;同时此时还要注意不要再“并加上新的索引”了,因为根本没有发生实质上的交换。
  3. 返回随机数就很简单了,随机返回数组里的一个数字即可。这也是数组的优势:可以指定位置随机访问。

空间复杂度: O(N)
时间复杂度: 全部操作都是 O(1)

class RandomizedCollection {
public:
    unordered_map<int, unordered_set<int>> idx;
    vector<int> nums;

    /** Initialize your data structure here. */
    RandomizedCollection() {

    }
    
    /** Inserts a value to the collection. Returns true if the collection did not already contain the specified element. */
    bool insert(int val) {
        nums.push_back(val);
        idx[val].insert(nums.size() - 1);
        return idx[val].size() == 1;
    }
    
    /** Removes a value from the collection. Returns true if the collection contained the specified element. */
    bool remove(int val) {
        if (idx.find(val) == idx.end()) {
            return false;
        }
        int i = *(idx[val].begin()); //解引用,获取val在数组中的位置,由于unordered_set是无序的,这里随便取集合的头部元素就行。
        nums[i] = nums.back();
        idx[val].erase(i);
        idx[nums[i]].erase(nums.size() - 1);
        if (i < nums.size() - 1) {
            idx[nums[i]].insert(i);
        }
        if (idx[val].size() == 0) {
            idx.erase(val);
        }
        nums.pop_back();
        return true;
    }
    
    /** Get a random element from the collection. */
    int getRandom() {
        return nums[rand() % nums.size()];
    }
};

尝试用vector替代unordered_set,被测试用例毒打

看了官方解答,我就想皮一下,把哈希表里的unordered_set换成vector不行嘛?还真的不行。因为每次要交换删除元素和尾部元素,vector很难处理上述的特殊情况,像下面这段代码,已经很折腾了:

class RandomizedCollection {
public:
    /** Initialize your data structure here. */
    RandomizedCollection() {}
    
    /** Inserts a value to the collection. Returns true if the collection did not already contain the specified element. */
    bool insert(int val) {
        v.emplace_back(val);
        M[val].emplace_back(v.size()-1);
        return M[val].size()==1;
    }
    
    /** Removes a value from the collection. Returns true if the collection contained the specified element. */
    bool remove(int val) {
        if(M.find(val)==M.end()) return false;
        int pos = M[val].back();
        M[val].pop_back();
        if(!M[v.back()].empty() && M[v.back()].back()==v.size()-1) M[v.back()].pop_back(); //如果尾部的元素的出现位置数组非空,那么就删除最后一个位置的索引
        v[pos] = v.back();
        if(pos<v.size()-1){
            M[v.back()].emplace_back(pos);
        }
        if(M[val].empty()){
            M.erase(val);
        }
        v.pop_back();
        return true;
    }
    
    /** Get a random element from the collection. */
    int getRandom() {
        return v[rand()%v.size()];
    }
private:
    vector<int> v;
    unordered_map<int,vector<int>> M;
};

跑不过这个测试用例:
在这里插入图片描述
因为最后一次删除30应该是无效的,但由于代码逻辑上的硬伤,最后一次删除30本应该失败的,但是却成功了…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值