哈希表
理论基础
文章讲解:代码随想录
哈希表
哈希表是 根据关键码的值而进行直接访问的数据结构
直白的说,数组就是哈希表的一种,数组索引就是哈希表的关键码,然后通过下标直接访问数组元素.
哈希表一般用来快速判断一个元素是否在集合里,并且复杂度为O(1).
哈希函数
哈希函数可以把元素直接映射为哈希表上的索引然后就可以通过查询索引下标快速判断元素是否在集合中
为了保证元素的映射出来的索引都在哈希表上,我们还需要对数值进行一个取模的操作.
哈希函数的设计目标是为了让不同关键字的冲突尽可能的少
常见的哈希函数:
-
除留取余法:H=key%p
哈希表长为m,取一个不大于m但最接近或等于m的质数p(用质数取余,分布更均匀,冲突更少;但是也要根据实际情况合理选择p的值,不要死板教条化)
-
直接定址法:H=key或H=key*a+b
其中a、b是常数,这种方法适合于关键字的分布基本连续的情况,若关键字的分布不连续,则会造成存储空间的浪费。
-
平方取中法:去关键字的平方值的中间几位作为哈希值
哈希碰撞
如果两个元素映射到了哈希表中的同一个位置,这就叫哈希碰撞
一般解决哈希碰撞的方法有两种:拉链法,线性探测法
拉链法:
将发生冲突的元素都存放在链表中,这样我们就可通过索引找到它们了.
线性探测法:即发生冲突时, 每次往后探测相邻的下一个单元是否为空。
使用该方法时,一定要保证数据长度大于哈希表长度,我们需要依靠哈希表中的空位来解决碰撞问题
常见的三种哈希结构
-
数组
-
set(集合)
-
map(映射)
在C++中,set和map分别提供以下三种数据结构
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
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) |
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
242.有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1: 输入: s = "anagram", t = "nagaram" 输出: true
示例 2: 输入: s = "rat", t = "car" 输出: false
说明: 你可以假设字符串只包含小写字母。
文章讲解:代码随想录
思路:
题目只包含了小写字母(26个),元素较少,我们可以用数组来解决这个问题。
每个字母的关键码可以用这个字母的ASCII减去a的ASCII码值来代表,那么字母a就对应0,字母b对应1,以此类推…
两个字符串是彼此的字母异位词,我们观察可以看出,两个字符串中的字母类型和数量都是完全一样的,只是排列顺序不同,那么我们可以用数组统计其中一个字符串中的每个字母的出现次数,然后再遍历另一个字符串,并把其中每个字符出现的次数减去,最后,我们再遍历一遍数组,如果数组中的元素都是0的话,那么这两个字符串就是字母异位词。
bool isAnagram(string s, string t) { int hash[26]={0}; for(int i=0;i<s.size();i++){ hash[s[i]-'a']++; //根据字母对应的关键码的值统计出现次数 } for(int i=0;i<t.size();i++){ hash[t[i]-'a']--; } for(int i=0;i<26;i++){ if(hash[i]!=0) return false; } return true; }
349. 两个数组的交集
文章讲解:代码随想录
思路:
题目要我们求两个数组的交集,并且输出的结果中是去重过的数据。
我们可以使用哈希表的方法来解决这道题,可以选择数组或者set数据结构,而使用数组是因为这道题限制了数据的大小,如果题目没有限制数据的大小,那么使用数组就会个非常浪费存储空间,所以这道题我们选择set。
在C++中提供了三种set的数据结构:
-
std::unorderedset
-
std::set
-
std::multiset
std::set和std::multiset的底层实现都是红黑树,而std::unorderedset的底层是哈希表,orderedset的读写效率是最高的,并不需要对数据进行排序, 而且还要让数据不重复(unorderedset会对数据进行自动去重)
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) { unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重 unordered_set<int> nums_set(nums1.begin(), nums1.end()); for (int num : nums2) { // 发现nums2的元素 在nums_set里又出现过 if (nums_set.find(num) != nums_set.end()) { result_set.insert(num); } } return vector<int>(result_set.begin(), result_set.end()); }
202题. 快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
示例:
输入:19 输出:true 解释: 1^2 + 9^2 = 82 8^2 + 2^2 = 68 6^2 + 8^2 = 100 1^2 + 0^2 + 0^2 = 1
文章讲解:代码随想录
思路:
题目中说这个正整数的每个位置上的数字平方和在这个重复的过程中可能会无限循环,也可能会变为1.既然会无限循环,那么,这个我们就可以判断这个sum在之前出现过的和中是否出现过,如果出现过,就不是快乐数,变为1,就是快乐数.很明显可以用哈希表来解决这个问题。
// 取数值各个位上的单数之和 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. 两数之和
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
文章讲解:代码随想录
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 {};
STL看不太懂,还没深入学习过,看着部分的代码有些用法有点懵,友友们就直接看卡哥的讲解吧,我再去进修一下再来回头看看这部分的.先打卡了o.O