哈希表基础理论
记录重中之重:
- 解决哈希碰撞的方法:a、拉链法(拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。)
b、线性探测法(依靠空位来解决碰撞——>碰撞后向附近寻找空位,一定要保证tableSize大于dataSize)
| 集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
|---|---|---|---|---|---|---|
| std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
| std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
| std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
| 映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
|---|---|---|---|---|---|---|
| std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
| std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) |
| std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。同理,需要键值对的时候也按这个优先级选择map的种类来使用。
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
242.有效的字母异位词
思路:数组是一个简单的哈希表,使用哈希场景时,首先看数组的范围是否有限,有限则优先使用数组。只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。
// 用一个size为26的数组去记录对应字母出现的次数
//时间复杂度: O(n)
//空间复杂度: O(1)
bool isAnagram(string s, string t) {
int record[26] = {0};
// 记录s中出现的字母和次数
for(int i = 0;i < s.size(); ++i) {
++record[s[i] - 'a'];
}
// 用以上记录核对t中出现的字母和次数
for(int i = 0; i < t.size(); ++i) {
--record[t[i] - 'a'];
}
// 检查记录是否吻合条件
for (int i = 0; i < 26; ++i) {
if (record[i] != 0) {
return false;
}
}
return true;
}
349. 两个数组的交集
// 使用unordered_set来去重加比较
//时间复杂度: O(mn)
//空间复杂度: O(n)
vector<int> intersection1(vector<int>& nums1, vector<int>& nums2) {
// 存储结果的集合
unordered_set<int> res_set;
// 存储nums1数据的集合,去重
unordered_set<int> nums_set(nums1.begin(),nums1.end());
// 比较得到两集合的交集,插入到结果集合中
for(auto &num : nums2) {
if (nums_set.find(num) != nums_set.end()) {
res_set.insert(num);
}
}
// 将结果转换为vector类型的返回值
return vector<int> (res_set.begin(), res_set.end());
}
//考虑到以下条件时:
//1 <= nums1.length, nums2.length <= 1000
//0 <= nums1[i], nums2[i] <= 1000
//可使用数组来解决本题,数组占用空间比set小,速度比set快,无边界时无法使用数组.
vector<int> intersection2(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> res_set;
int hash[1001] = {0};
for(int &num:nums1) {
hash[num] = 1;
}
for(int &num:nums2) {
if (hash[num] == 1) {
res_set.insert(num);
}
}
return vector<int> (res_set.begin(), res_set.end());
}
第202题. 快乐数
思路:计算出getnextnumber的时间复杂度就可以了 求下一个n的时间复杂度是logn 因为log10n可以算出n有几个digit 所以第一次循环循环了logn次 第二个循环loglogn 以此类推 取大头就是logn 空间复杂度因为用了一个额外的哈希集合所以是logn。
class Solution {
public:
// 求n每一位的平方和
int sumRes(int n) {
int sum = 0;
while (n) {
int tmp = (n % 10) * (n % 10);
sum += tmp;
n /= 10;
}
return sum;
}
// 时间复杂度:O(log n)
// 空间复杂度:O(log n)
bool isHappy(int n) {
unordered_set<int> res_set;
while(1) {
int sum = sumRes(n);
if (sum == 1) {
return true;
}
// 找到了说明已经插入过该值,说明开始循环
if (res_set.find(sum) != res_set.end()) {
return false;
}
else {
res_set.insert(sum);
}
n = sum;
}
}
1. 两数之和
思路:什么时候使用哈希法,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。因为本地,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
因为本地,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
需要明确两点:
- map用来做什么
- map中key和value分别表示什么
// 先试试暴力解法,好对比 O(n^2)
vector<int> twoSum1(vector<int>& nums, int target) {
for (int i = 0; i < nums.size(); ++i) {
for (int j = i + 1; j < nums.size(); ++j) {
if (nums[i] + nums[j] == target) {
vector<int> sum = {i, j};
return sum;
}
}
}
vector<int> sum;
return sum;
}
// 使用map来遍历存储数组的key-value,
// key是数组的下标,value是对应下标的元素值
// 时间复杂度: O(n)
// 空间复杂度: O(n)
vector<int> twoSum2(vector<int>& nums, int target) {
unordered_map<int, int> res_map;
for (int i = 0; i < nums.size(); ++i) {
// 遍历当前元素,在map中寻找匹配的key
auto it = res_map.find(target - nums[i]);
if (it != res_map.end()) {
return {it->second, i};
}
// insert 函数的使用方式是 res_map.insert(make_pair(key, value)); 或者 res_map.insert({key, value});
// 匹配失败,则把当前元素和下标加入到map中
res_map.insert(make_pair(nums[i], i));
}
return {};
}