- 哈希表是什么?
本质上来说哈希表只是一个存储数据的容器
- 为什么用哈希表?
哈希表查找元素的速度很快, O(1)级别; 但是代价是O(n)的空间复杂度, 属于空间换时间
- 什么时候用哈希表?
频繁的查找某个数的时候, 但是并不是所有的查找都用哈希表, 对顺序排列的元素用二分搜索O(logN)
-
怎么用哈希表?
-
一般常用的高级语言的库都会提供哈希表, 直接使用即可
-
用数组模拟一个简易的哈希表, 这样会比容器更快
比如字符串里的字符, 如果全是大写/小写英文字母, int hash[26]; hash[c-'A']即为c字符的个数, 键值对的形式是: <index, hash[index]> ,value可以是字符出现的次数, 索引等等
数据范围很小的时候,整数的范围在 [1~10^x], x在[3,7] , 但是出现负数就不推荐使用数组, 因为需要加一个偏移量, 比较麻烦.
我们同样可以用哈希函数去模拟哈希表, 但是可能没必要
题目1: 两数之和
1. 暴力解法
暴力解法有两种思路:
第一种是从 i=0 开始, j = [i+1 , nums.size()-1] 依次遍历剩下的元素查找是否有和为target的元素.
第二种也是从i=0开始, 但是j从i开始向前寻找, 也是遍历了所有的元素, 时间复杂度没有任何区别.
第二种:
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for(int i = 0; i < nums.size(); i++)
{
for(int j = i; j > 0;j--)
{
if(nums[i] + nums[j-1] == target)
return {i,j-1};
}
}
return {0,0};
}
};
2. 哈希表
哈希的思路是从第二种暴力解法得来的, 哈希表里存储<数值, index>.
i 也是从 0 开始向后遍历, 查找另一个元素实际可以转化成查找hash表里是否有 target-nums[i] 这个key, 如果有返回{i, hash[target-nums[i]] } 即可. 没有就在hash里添加这个键值对, 实质是把寻找的过程从 O(n) 降为了 O(1)
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> hash;
int i = 0;
while(i < nums.size())
{
int num = target-nums[i];
if(hash.find(num) != hash.end())
return {i, hash[num]};
hash[nums[i]] = i;
i++;
}
return {0,0};
}
};
从第一种暴力解法延伸而来的哈希也可以, 但是由于在第一遍寻找时就把所有元素加入到了哈希表, 因此如果一个值 2*nums[i] == target, 那它会查找到自身返回{i,i}因此需要特殊处理这种情况
题目2: 判断是否互为字符重排
思路: 如果s1重排后能得到s2, 那么s1中每个字符出现的个数 一定等于s2中对应字符出现的个数
写法1: 两个哈希表统计s1和s2字符个数, 然后比较即可:
cpp
class Solution {
public:
bool CheckPermutation(string s1, string s2) {
if(s1.length() != s2.length())
return false;
unordered_map<char,int> hash1;
unordered_map<char,int> hash2;
for(int i = 0; i < s1.length();i++)
{
hash1[s1[i]]++;
hash2[s2[i]]++;
}
for(const auto& e : hash1)
{
if(hash2[e.first] != e.second)
return false;
}
return true;
}
};
写法2: 只使用一个哈希表统计 s1 的字符出现次数, 再遍历 s2 依次对hash表元素次数减一, 如果可以重排, 最终哈希表所有元素的值都应该是0, 如果出现负值, 则无法重排.
cpp
class Solution {
public:
bool CheckPermutation(string s1, string s2) {
if(s1.length() != s2.length())
return false;
int hash[26] = {0};
for(int i = 0; i < s1.length();i++)
hash[s1[i]-'a']++;
for(int i = 0; i < s2.length();i++)
{
if(--hash[s2[i]-'a'] < 0)
return false;
}
return true;
}
};
注意: 两种写法都需要对长度不相等的字符串特殊处理, 长度不相等一定无法重排
题目3: 存在重复元素 I
和两数之和思路类似, 遍历一次向前查找hash里是否有数存在, 不存在就存入哈希表, 继续遍历:
cpp
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
unordered_set<int> hash;
for(int i = 0; i < nums.size(); ++i)
{
if(hash.count(nums[i]))
return true;
hash.insert(nums[i]);
}
return false;
}
};
题目4: 存在重复元素 I I
这题和存在重复元素I 类似, 也是遍历一次向前查找哈希表即可, 但是每次寻找还需要比较下标之差gap和k的关系, 因此需要用unordered_map存储下标.
注意: 有一个细节需要注意, 因为unordered_map相同的key只能存在一个, 所以下一次遇到相同元素如果不满足gap <= k, 要覆盖吗?
要, 因为我们是从前向后遍历, gap = j - i 的值会随 j 的增大而增大, 所以如果本次gap > k, 随着 j 增大gap会越来越大, 不可能符合要求. 所以要覆盖掉原来的 i , 这样使之后gap的值变得更小, 更容易满足要求.
cpp
class Solution {
public:
bool containsNearbyDuplicate(vector<int>& nums, int k) {
unordered_map<int, int> hash;
for(int i = 0; i < nums.size(); i++)
{
if(hash.count(nums[i]))
{
if(i - hash[nums[i]] <=k)
return true;
}
hash[nums[i]] = i;
}
return false;
}
};
题目5: 字⺟异位词分组
此题是题目2的升级版, 需要把字母异位词全组合到一起, 这题充分体现出hash容器和泛型编程的好处.
对于所有的字母异位词, 它们按照 某种规则排序后的词字母顺序一定是一样的, 因此利用一个unordered_map<string, vector<string>>, 将一类字母异位词全部映射到一个vector里即可:
cpp
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> hash;
vector<vector<string>> ret;
for(const auto& s : strs)
{
string tmp = s;
sort(tmp.begin(),tmp.end());
hash[tmp].push_back(s);
}
for(auto & pair: hash)
ret.push_back(pair.second);
return ret;
}
};
暂时结束