380. O(1) 时间插入、删除和获取随机元素
实现RandomizedSet 类:
RandomizedSet() 初始化 RandomizedSet 对象
bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。
bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。
int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。
你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。
示例:
输入
[“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 。
提示:
−
2
31
<
=
v
a
l
<
=
2
31
−
1
-2^{31} <= val <= 2^{31} - 1
−231<=val<=231−1
最多调用 insert、remove 和 getRandom 函数
2
∗
1
0
5
2 * 10^5
2∗105 次
在调用 getRandom 方法时,数据结构中 至少存在一个 元素。
以O(1)时间复杂度来进行插入、删除,很容易想到哈希表,但是如何以O(1)时间复杂度来返回随机元素呢?这个哈希表做不到,但是数组可以随机下表访问。
但是,虽然数组可以随机下标访问,却无法做到O(1)时间的删除,我们需要进行遍历,找到对应元素后,然后才能进行处理。
采用有序数组的话,依然无法保证在O(1)时间内完成。
最后,参考了题解,发现是采用了变长数组+哈希表的方式来实现的,变长数组储存数值,哈希表来存储数值对应的下标索引,这样插入和删除都可以采用O(1)的复杂度来完成,同时维护这两个数据结构。而返回随机一个元素,可以通过生成随机数的方式,以O(1)的时间复杂度完成,十分巧妙。
代码实现较为简单,如下:
#include <vector>
#include <unordered_map>
#include <ctime>
#include <cstdlib>
using namespace std;
class RandomizedSet
{
private:
vector<int> vals;
unordered_map<int, int> valsToIndex;
public:
RandomizedSet()
{
srand(time(0));
}
bool insert(int val)
{
if (valsToIndex.find(val) != valsToIndex.end())
{
return false;
}
valsToIndex[val] = vals.size();
vals.push_back(val);
return true;
}
bool remove(int val)
{
if (valsToIndex.find(val) == valsToIndex.end())
{
return false;
}
vals[valsToIndex[val]] = vals[vals.size() - 1];
valsToIndex[vals[vals.size() - 1]] = valsToIndex[val];
vals.pop_back();
valsToIndex.erase(val);
}
int getRandom()
{
return vals[rand() % vals.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();
*/
384. 打乱数组
给你一个整数数组 nums ,设计算法来打乱一个没有重复元素的数组。打乱后,数组的所有排列应该是 等可能 的。
实现 Solution class:
Solution(int[] nums) 使用整数数组 nums 初始化对象
int[] reset() 重设数组到它的初始状态并返回
int[] shuffle() 返回数组随机打乱后的结果
示例 1:
输入
[“Solution”, “shuffle”, “reset”, “shuffle”]
[[[1, 2, 3]], [], [], []]
输出
[null, [3, 1, 2], [1, 2, 3], [1, 3, 2]]
解释
Solution solution = new Solution([1, 2, 3]);
solution.shuffle(); // 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。例如,返回 [3, 1, 2]
solution.reset(); // 重设数组到它的初始状态 [1, 2, 3] 。返回 [1, 2, 3]
solution.shuffle(); // 随机返回数组 [1, 2, 3] 打乱后的结果。例如,返回 [1, 3, 2]
提示:
1 <= nums.length <= 50
−
1
0
6
<
=
n
u
m
s
[
i
]
<
=
1
0
6
-10^6 <= nums[i] <= 10^6
−106<=nums[i]<=106
nums 中的所有元素都是 唯一的
最多可以调用
1
0
4
10^4
104 次 reset 和 shuffle
利用洗牌算法,从后到前遍历数组,把当前位置中的元素与前面的一个随机元素相互交换即可。
#include <vector>
#include <cstdlib>
#include <ctime>
using namespace std;
class Solution
{
vector<int> &original, nums;
public:
Solution(vector<int> &nums) : original(nums), nums(nums) {}
vector<int> reset()
{
return original;
}
vector<int> shuffle()
{
for (int i = nums.size() - 1; i > 0; i--)
{
swap(nums[i], nums[rand() % (i + 1)]);
}
return nums;
}
};
/**
* Your Solution object will be instantiated and called as such:
* Solution* obj = new Solution(nums);
* vector<int> param_1 = obj->reset();
* vector<int> param_2 = obj->shuffle();
*/
387. 字符串中的第一个唯一字符
给定一个字符串 s ,找到 它的第一个不重复的字符,并返回它的索引 。如果不存在,则返回 -1 。
示例 1:
输入: s = “leetcode”
输出: 0
示例 2:
输入: s = “loveleetcode”
输出: 2
示例 3:
输入: s = “aabb”
输出: -1
提示:
1
<
=
s
.
l
e
n
g
t
h
<
=
1
0
5
1 <= s.length <= 10^5
1<=s.length<=105
s 只包含小写字母
自己写的代码如下:
#include <string>
#include <map>
using namespace std;
class Solution
{
public:
int firstUniqChar(string s)
{
map<char, int> mp;
for (int i = 0; i < s.size(); i++)
{
mp[s[i]]++;
}
for (int i = 0; i < s.size(); i++)
{
if (mp[s[i]] == 1)
return i;
}
return -1;
}
};
但是,使用hash造成的时间开销依然较大。
由于s的元素范围为小写字母,因此,可以使用数组进行优化,无非就是一个长度为26的int类型数组,节省了hash的时间,进一步降低了时间开销。
class Solution
{
public:
int firstUniqChar(string s)
{
int count[26];
for (int i = 0; i < 26; i++)
{
count[i] = 0;
}
for (int i = 0; i < s.size(); i++)
{
count[s[i] - 'a']++;
}
for (int i = 0; i < s.size(); i++)
{
if (count[s[i] - 'a'] == 1)
{
return i;
}
}
return -1;
}
};
395. 至少有 K 个重复字符的最长子串
给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。
如果不存在这样的子字符串,则返回 0。
示例 1:
输入:s = “aaabb”, k = 3
输出:3
解释:最长子串为 “aaa” ,其中 ‘a’ 重复了 3 次。
示例 2:
输入:s = “ababbc”, k = 2
输出:5
解释:最长子串为 “ababb” ,其中 ‘a’ 重复了 2 次, ‘b’ 重复了 3 次。
提示:
1
<
=
s
.
l
e
n
g
t
h
<
=
1
0
4
1 <= s.length <= 10^4
1<=s.length<=104
s 仅由小写英文字母组成
1
<
=
k
<
=
1
0
5
1 <= k <= 10^5
1<=k<=105
这道题目虽然是中等难度,但自己并没有AC出来。后来也是参考了题解的做法,才将其重写出来,所以感觉还是有一定难度的。题解给了两种做法,分别是dfs和滑动窗口,都不好想。
dfs的思路主要是,如果我找到了一个字符出现次数<k,那么最终的结果中一定是不包含这个字符的,依据此,就可以对原有字符串进行分割,然后继续dfs,知道找到满足条件的解(即,split==0),然后return。
class Solution
{
int dfs(const string &s, int l, int r, int k)
{
vector<int> vec(26);
for (int i = l; i <= r; i++)
{
vec[s[i] - 'a']++;
}
char split = 0;
for (int i = 0; i < 26; i++)
{
if (vec[i] > 0 && vec[i] < k)
{
split = 'a' + i;
break;
}
}
if (split == 0)
{
return r - l + 1;
}
int i = l;
int ret = 0;
while (i <= r)
{
while (i <= r && s[i] == split)
{ // 跳过split字符
i++;
}
if (i > r)
{
break;
}
int start = i;
while (i <= r && s[i] != split)
{ // 找到一个被split字符分割的串
i++;
}
// 在split分割的串内部,继续寻找满足条件的字串
int lenth = dfs(s, start, i - 1, k);
ret = max(ret, lenth);
}
return ret;
}
public:
int longestSubstring(string s, int k)
{
return dfs(s, 0, s.size() - 1, k);
}
};
滑动窗口的思路也不好想,主要为:由于字符串中总共只有26个字母,最终的字串也一定是由这26个字母组成的,那么就通过滑动窗口进行一个遍历(子串中包含一个字符的情况,到字串中包含26个字符的情况)。遍历的过程中,如果遇到less==0,就说明找到了一个可行解,记录下来,然后继续遍历。最后返回结果。
class Solution
{
public:
int longestSubstring(string s, int k)
{
int res = 0;
for (int i = 0; i <= 26; i++)
{
int l = 0, r = 0;
vector<int> vec(26);
int tot = 0;
int less = 0;
while (r < s.size())
{
// 窗口扩张后,更新tot和less变量
vec[s[r] - 'a']++;
if (vec[s[r] - 'a'] == 1)
{
less++;
tot++;
}
if (vec[s[r] - 'a'] == k)
{
less--;
}
while (tot > i)
{ // 进行窗口收缩
vec[s[l] - 'a']--;
if (vec[s[l] - 'a'] == k - 1)
{
less++;
}
if (vec[s[l] - 'a'] == 0)
{
tot--;
less--;
}
l++;
}
if (less == 0)
{ // 找到了一个可行解
res = max(res, r - l + 1);
}
// 窗口扩张
r++;
}
}
return res;
}
};