算法训练营 第六天
解锁新章节 哈希表day01
哈希表理论基础
1、哈希表的妙用
哈希表主要是能够实现快速查找,查找复杂度为o(1),和数组的查找复杂度一样,那为什么不用数组,还需要哈希表呢,
- 增删操作,哈希更快
因为数组每次增删数据,都需要重新在内存中查找合适的内存空间,然后搬过去,但哈希表就不用,哈希表类似在内存中开辟了一块空间,足够用一段时间,等到不够用了,或者产生哈希碰撞了,就在碰撞的地方生成链表存储(拉链法),或者向下查找有没有空位,给他放进去(线性探测法),
- 哈希表适用于根据部分信息查找的情况
在实际应用中经常只知道部分信息,用部分的信息查找全部的信息,比如已知用户id,要查找用户的所有信息,数组只能通过全部信息来查找,用部分信息来查找,它的时间复杂度为o(n),而哈希表是key-value的键值形式,将用户id设为key时,通过特定的哈希函数将key转换为一个哈希值,存储在哈希表相应的位置,因此它的查找复杂度为o(1)。
2、哈希表怎么实现时间复杂度为o(1)的
当知道数组的下标时,查找该下标对应的值,时间复杂度是o(1)。哈希表也类似,key可以通过哈希函数,转化为一个哈希值,这个哈希值基本都是一个int类型的数(如果不对,还请大佬指正),也就是起到数组下标的作用,这样在已知key时,就能直接查找到下标对应的值了。因此哈希表和数组一样,是一段连续的空间,注意当发生哈希碰撞,用链表来延申的时候,那段空间不连续,同时查找时间复杂度也不是o(1)了。
242.有效的字母异位词
这道题我的思路是:
1、按需建立哈希表,如果哈希表里有字符a,就hash[a]++,如果没有,就插入键值对(a,1)
for (auto a : s) {
if (hash.find(a) != hash.end())
hash[a]++;
else
hash.insert(make_pair(a, 1));
}
2、然后处理字符串t,遍历字符串t,如果哈希表里没有字符a,则返回false,如果有就hash[a]–。如果hash[a]<0了就返回false。这里就有小伙伴要疑惑了,那如果字符串s里有两个字符b,字符串t里有一个字符b,那hash[‘b’]==1,不会小于0,这种应该怎么办呢,这个时候,我们需要在函数最开始判断一下,字符串s和字符串t的大小是否相等,如果字符串大小相等,且字符串t里不存在字符串s里没有的元素,这样的话,如果s和t不是字母异位词,那就一定会有字符hash[a]<0。
for (auto a : t) {
//如果t里存在s没有的字符,则直接返回false
if (hash.find(a) == hash.end())
return false;
hash[a]--;
if (hash[a] < 0)
return false;
}
源码:
class Solution {
public:
bool isAnagram(string s, string t) {
unordered_map<char, int> hash;
if (s.size() != t.size())
return false;
for (auto a : s) {
if (hash.find(a) != hash.end())
hash[a]++;
else
hash.insert(make_pair(a, 1));
}
for (auto a : t) {
//如果t里存在s没有的字符,则直接返回false
if (hash.find(a) == hash.end())
return false;
hash[a]--;
if (hash[a] < 0)
return false;
}
return true;
}
};
参考答案(代码随想录)
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++) {
// 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
record[s[i] - 'a']++;
}
for (int i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (record[i] != 0) {
// record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
// record数组所有元素都为零0,说明字符串s和t是字母异位词
return true;
}
};
代码随想录中的参考答案没有使用哈希表,而是直接创建了空间大小为26的数组,然后用a-z来表示数组的下标。按理说我是按需创建哈希,内存使用会更小,且不用结尾再遍历一遍数组,时间复杂度也会更小,但是结果显示内存占用会更多,我猜是因为哈希表创建时,就已经分配了一些内存,这个内存比26个大小更大,因此内存占用更大。
349. 两个数组的交集
这道题首先想到用哈希,我用的unorder_map,第一个值是int,第二个值(value值)设置为布尔值。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, bool>hash;
vector<int> res;
for (auto a : nums1) {
if (hash.find(a) == hash.end())
hash.insert(make_pair(a, true));
}
for (auto a : nums2){
if (hash.find(a) != hash.end() && hash[a] == true) {
res.push_back(a);
hash[a] = false;
}
}
return res;
}
};
代码随想录中的参考答案用的是unorder_set,用set确实合理,但这样的话就没法保证结果数组的去重,因此它结果数组也是用的unorder_set,实现了去重的操作,但是内存占用会相对更多一些。
时间复杂度和空间复杂度上,我的答案都更优一些。
202. 快乐数
这道题的思路比较明晰,如果这个数不是快乐数,那可能显然死循环,也就是有些数会重复出现,因此用unorder_set记录之前出现过的数,如果中间计算的数有重复出现的,则该数一定不是快乐数。
然后如何得到每个位置上的数字的平方和,也是这道题的一个重点
我的源码
class Solution {
public:
bool isHappy(int n) {
unordered_set<int> hash_set;
while (true) {
int tmp = 0;
while (n) {
tmp += (n % 10) * (n % 10);
n /= 10;
}
n = tmp;
if (n == 1)
return true;
if (hash_set.find(n) != hash_set.end())
return false;
hash_set.insert(n);
}
}
};
参考答案
class Solution {
public:
// 取数值各个位上的单数之和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
1. 两数之和
经典两数之和,经典到我看到他的第一个想法就是暴力解法,就是两层for循环遍历。遍历就是为了先确定一个数,然后再判断另一个数在不在数组里,这,不就是经典哈希判断元素是否存在的问题吗?因此,用哈希!
因为要考虑重复元素的问题,所以我们用multimap(比如题目给的数组为{3,3},那么这两个数都要存到表里,需要用可以重复键的结构),key里存数组里的值,value存数组下标。将数组遍历存进multimap里,然后再遍历哈希表判断。
这里重复值的处理有点别扭,以输入:vector = {3,3} value = 6为例,multimap存储内容为{[3,0],[3,1]},用迭代器遍历,iter->first ==3,分两种情况
1、value-iter->first==iter->first时(即6-3=3)
那么先判断3在map里有几个,用hash.count(),如果只有一个,那个continue;如果不止一个,则返回iter->second和(++iter)->second(因为multimap是有顺序的,所以下一个3一定在iter的后一个)
2、value-iter->first!=iter->first时
那么正常查找value-iter->first在不在map里
源码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
multimap<int, int>hash;
for (int i = 0; i < nums.size(); i++) {
hash.insert(make_pair(nums[i], i ));
}
for (auto iter = hash.begin(); iter != hash.end(); iter++) {
int tmp = target - iter->first;
if (tmp == iter->first) {
if (hash.count(tmp) != 1) {
return{ iter->second, (++iter)->second };
}
else
continue;
}
if (hash.find(tmp) != hash.end())
return {iter->second, (hash.find(tmp))->second};
}
return { 0,0 };
}
};
这道题参考答案的方法更优,它最开始把数组存到map的过程中,边存边查。还是以{3,3},6为例,先把第一个3存进去,然后存第二个3时先判断value-vec[1]这个值是否在map里,在的话就直接返回了,不在,再把第二个3存进去,这样就解决了我之前碰到的因为有重复值而不得不使用multimap的问题,而是可以用unordered_map啦,unordered_map的底层实现是哈希表,查找效率是o(1),multimap的底层实现是红黑树,查找效率是o(logn),所以能用unordered_map就用unordered_map。
参考答案
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map <int,int> map;
for(int i = 0; i < nums.size(); i++) {
// 遍历当前元素,并在map中寻找是否有匹配的key
auto iter = map.find(target - nums[i]);
if(iter != map.end()) {
return {iter->second, i};
}
// 如果没找到匹配对,就把访问过的元素和下标加入到map中
map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
结束!今天花的时间最长的就是这个两数之和,没想到用边存边查的方法来避免重复值的情况。今天花了大概3个小时吧,但是因为上午拉肚子老跑厕所,没有在上午弄完,时间他不等人啊呜呜,明天继续加油