@[TOC](【小白爬Leetcode381】O(1) 时间插入、删除和获取随机元素 - 允许重复 Convert BST to Greater Tree)
题目
Leetcode381
h
a
r
d
\color{#ff0000}{hard}
hard
点击进入原题链接:O(1) 时间插入、删除和获取随机元素 - 允许重复
第一想法:
当时第一想法是:O(1)的时间插入、删除,这不就是哈希表的典型应用嘛!至于这个random…生成随机数?
于是有了下面这种解法:
- 维护一个哈希表,hashKey用插入的数字计算,value则代表这个数字出现了几次
- 维护一个Solution类的成员变量
int num
,表示一共有多少个数字,每插入一个新的数this->num++
,每删除一个存在的数字,this->num--
- 返回随机数的时候,
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
。数组用来存放数字,哈希表用来统计每个数字在数组中的所有位置
- 插入元素的时候,直接向数组
V
数组尾插val
,并向哈希表中M[val]
所映射的集合(unordered_set)中插入当前数组尾部的索引V.size()-1
。说人话就是数组尾部插入数字,并在哈希表里记录这个数字在尾部的位置。 - 删除元素的时候,首先通过哈希表
M
获取当前元素在数组V
中的位置。获取val在数组V
中的位置,由于unordered_set是无序的,这里随便取集合的头部元素*(idx[val].begin)
就行。
获取到位置之后,肯定不能直接在数组V
中删除该位置的元素,这样会导致数组后面的元素集体往前挪一位,肯定达不到O(1)的时间复杂度。因此采用当前位置的数字和数组尾部数字互换,然后把数组尾删一个元素即可。注意,这里的难点在于交换尾删的同时要更新哈希表M
,被删除的数字在哈希表中要删除当前位置索引,最后一个数字在哈希表中要删去原来最后一个位置索引,并加上新的索引。
这里有一个特殊情况,即如果要删除的数字本来就在数组的最后一个位置,那么同一个位置会被删除两遍,好在unordered_set
的erase
方法可以处理删除的元素不存在的情况;同时此时还要注意不要再“并加上新的索引”了,因为根本没有发生实质上的交换。 - 返回随机数就很简单了,随机返回数组里的一个数字即可。这也是数组的优势:可以指定位置随机访问。
空间复杂度: 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本应该失败的,但是却成功了…