文章目录
1、字母异位分组
题目
分析
本题可以维护一个map,保存字符串和在输出的vector中的下标,难点在于怎么去确认字符是否在这一数组中。
关键在于存在map中的string,这里我们存在里面的应该排序后的字符串,然后把传入的每个字符串先保留一个副本,然后排序了后在map中count。
复杂度
时间复杂度:O(n)
空间复杂度:O(n)
2、无重复字符的最长子串
分析
这题是很标准的hash题,关键在于这里的 set 要以字符为键值。
维护一个指针 left 指向不重复子串的开始位置,当 right 移动到出现重复的字符处时就更新 left 的位置和 set 中的记录,然后用 ret 记录无重复子串长度,也即当前位置减 left 然后加1。如下所示:
int lengthOfLongestSubstring(string s) {
int n=s.size();
if(n==0) return 0;
unordered_set<char> us;
int left=0,right=0;
int ret=0;
while(right<n){
if(us.count(s[right])){
while(s[left]!=s[right]){
us.erase(s[left++]);
}
//因为这里s[left]==s[right],所以left应当再加1
++left;
}
else{
us.insert(s[right]);
}
ret=max(ret,right-left+1);
++right;
}
return ret;
}
复杂度
时间复杂度:O(n)
空间复杂度:O(n)
3、重复的DNA序列(187)
题目
分析
这里先记录下string的substr函数,他可以从指定位置截取指定长度的子字符串,如:
string a=s.substr(i,10);
a就是s从i位置开始(包括他自己)截取长度为10的字符串。
这里维护两个hash_set,一个记录每个子字符串,一个记录重复的字符串,然后从头开始遍历即可。
注:记录重复的字符串的hash_set是很有必要的,因为如果遇到13个a,不进行查重的话,结果会输出4个10个a的字符串。
复杂度
时间复杂度:O(n)
空间复杂度:O(n)
4、最小子覆盖串&找到字符串中的所有异位词
题目
分析
下面这个老哥针对滑动窗口相关的题,提出了如下的解法,写的相当好
https://leetcode-cn.com/problems/minimum-window-substring/solution/hua-dong-chuang-kou-suan-fa-tong-yong-si-xiang-by-/
他为这个类型的题写了一个模板如下:
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...
/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/
// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}
以438题为例,代码如下:
vector<int> findAnagrams(string s, string p) {
int n = s.size();
if(n==0||p.empty()) return {};
unordered_map<char, int> need, window;
for(int i=0;i<p.size();++i) ++need[p[i]];
int left = 0, right = 0, vaild = 0;
vector<int> out;
while (right < n) {
char c = s[right];
++right;
if (need.count(c)) {
++window[c];
if (window[c] == need[c]) ++vaild;
}
if (right - left == p.size()) {
char d = s[left];
if (vaild == need.size()) out.push_back(left);
++left;
if (window.count(d)) {
if (window[d] == need[d]) --vaild;
--window[d];
}
}
}
return out;
}
复杂度
时间复杂度:O(n)
空间复杂度:O(n)
5、LRU缓存机制
题目
分析
这题算是道人气题目了,与其说是做题,倒不如说是在hash_map的基础上创造出一个新的数据结构。这里结合了hash_map的快速查找性和list的有序性。
在map中以key为键值,实值是list的迭代器,然后在list中存放一个由key和value组成的pair,如下图:
hashmap应当以int和list的迭代器作为值建立,如下:
unordered_map<int, list<pair<int, int>>::iterator> m;
注意:
(1)因为链表是前闭后开的,即end指向最后一个节点的下一位,即end指向最后一个节点的下一个节点。所以插入应该放在开头,因为在插入后要将迭代器存入map中,从首部插入,可直接返回begin()函数。取出操作则在结尾执行。
(2)因为删除时需要从链表尾部的节点定位到map,所以list中应该保存有key值。
复杂度
时间复杂度:O(1)
空间复杂度:O(n)
6、数组中重复的数字&缺失的第一个正数
分析
分析
这两题都使用了原地哈希的方法,即把数组视为哈希表。
1)由于数组元素的值都在指定的范围内,这个范围恰恰好与数组的下标可以一一对应。
2)看到数值,就可以知道它应该放在什么位置,这里数字nums[i] 应该放在下标为 i 的位置上,这就像是我们人为编写了哈希函数。
3)找到重复的数就是发生了哈希冲突。
数组中重复的数字
代码如下:
int findRepeatNumber(vector<int>& nums) {
int temp;
for(int i=0;i<nums.size();i++){
while (nums[i]!=i){
if(nums[i]==nums[nums[i]]){
return nums[i];
}
swap(nums[i],nums[nums[i]]);
}
}
return -1;
}
缺失的第一个正数
这里的思路是把 1 这个数放到下标为 0 的位置, 2 这个数放到下标为 1 的位置,按照这种思路整理一遍数组。然后我们再遍历一次数组,第 1个遇到的它的值不等于下标的那个数,就是我们要找的缺失的第一个正数。
代码如下:
int firstMissingPositive(vector<int>& nums) {
int n=nums.size();
for(int i=0;i<n;++i){
while(nums[i]!=i+1){
//下面三种情况都表示nums[i]是无效数,其中nums[i]==nums[nums[i]-1]表示出现重复
if(nums[i]>n||nums[i]<1||nums[i]==nums[nums[i]-1]) break;
//将nums[i]换到它应该待的位置上
swap(nums[i],nums[nums[i]-1]);
}
}
for(int j=0;j<n;++j){
if(nums[j]!=j+1) return j+1;
}
return n+1;
}
复杂度
时间复杂度:O(N)
空间复杂度:O(1)
7、前K个高频元素
题目
分析
这题拿到手后首先肯定要进行统计的,这里使用map来完成,
unordered_map<int,int> um;
for(int a:nums){
++um[a];
}
统计完成后,对于TOP K问题,可以使用优先队列来处理,
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> pq;
for(auto a:um){
if(pq.size()==k){
if(a.second>pq.top().first){
pq.pop();
pq.push(make_pair(a.second,a.first));
}
}
else pq.push(make_pair(a.second,a.first));
}
注意,这里由于要按照出现次数进行排序,所以make_pair要把map的实值作为first,键值作为second。
另外,这里priority_queue中使用的greater<pair<int,int>表示构造小顶堆,等同于使用,
struct cmp{
bool operator()(pair<int,int> a,pair<int,int> b){
//大的往下移
return a.first>b.first;
}
};
最后,将K个元素从优先队列中取出,
vector<int> ret;
for(int i=0;i<k;++i){
ret.push_back(pq.top().second);
pq.pop();
}
复杂度
时间复杂度:O(NlgK)
空间复杂度:O(N)
8、最长连续序列
题目
分析
这里因为时间复杂的限制,我们没办法使用排序。
这里的思路是先用哈希表把所有的数都存下来,然后再次遍历,然后根据哈希表查找最长的连续序列。这里为了避免重复查找,关键在于每次查询都从连续序列中最小的那个数开始。代码如下:
int longestConsecutive(vector<int>& nums) {
int n=nums.size();
unordered_set<int> us;
for(int a:nums) us.insert(a);
int maxlen=0;
for(int a:nums){
//这一步是关键,它避免了重复的查找。
if(us.count(a-1)) continue;
int len=1;
while(us.count(a+1)){
++a;
++len;
}
maxlen=max(len,maxlen);
}
return maxlen;
}
复杂度
时间复杂度:O(N)
空间复杂度:O(N)
9、LFU
题目
分析
这题的O(1)过于繁琐,所以我选择放弃。
这里采取官解提出的哈希map+红黑树set的O(lgN)的组合进行求解,先上官解代码,
struct Node {
int cnt, time, key, value;
Node(int _cnt, int _time, int _key, int _value):cnt(_cnt), time(_time), key(_key), value(_value){}
bool operator < (const Node& rhs) const {
return cnt == rhs.cnt ? time < rhs.time : cnt < rhs.cnt;
}
};
class LFUCache {
// 缓存容量,时间戳
int capacity, time;
unordered_map<int, Node> key_table;
set<Node> S;
public:
LFUCache(int _capacity) {
capacity = _capacity;
time = 0;
}
int get(int key) {
if (capacity == 0) return -1;
auto it = key_table.find(key);
// 如果哈希表中没有键 key,返回 -1
if (it == key_table.end()) return -1;
// 从哈希表中得到旧的缓存
Node cache = it -> second;
// 从平衡二叉树中删除旧的缓存
S.erase(cache);
// 将旧缓存更新
cache.cnt += 1;
cache.time = ++time;
// 将新缓存重新放入哈希表和平衡二叉树中
S.insert(cache);
it -> second = cache;
return cache.value;
}
void put(int key, int value) {
if (capacity == 0) return;
auto it = key_table.find(key);
if (it == key_table.end()) {
// 如果到达缓存容量上限
if (key_table.size() == capacity) {
// 从哈希表和平衡二叉树中删除最近最少使用的缓存
key_table.erase(S.begin() -> key);
S.erase(S.begin());
}
// 创建新的缓存
Node cache = Node(1, ++time, key, value);
// 将新缓存放入哈希表和平衡二叉树中
key_table.insert(make_pair(key, cache));
S.insert(cache);
}
else {
// 这里和 get() 函数类似
Node cache = it -> second;
S.erase(cache);
cache.cnt += 1;
cache.time = ++time;
cache.value = value;
S.insert(cache);
it -> second = cache;
}
}
};
因为同时对次数和时间都做了要求,所以这里专门构建的一个Node的结构体,以把四个元素都保存下来。
然后下面的对“<”的重载,涉及到红黑树,这里为使时间更进一步选择了红黑树而不是优先队列,同时还是因为优先队列不支持删除的操作。红黑树的顶部则应该保存在满的时候要删除的结点。根据题意首先先删除调用次数最少的元素,相同调用次数的情况下优先删除存在时间更短的那个元素。
这里设置了一个全局时间time,每次操作都将其加1,同时Node中自带一个cnt,表示到目前为止被调用的次数。