今日任务
- 哈希表理论基础
- 242.有效的字母异位词
- 349. 两个数组的交集
- 202. 快乐数
- 1. 两数之和
详细布置
哈希表理论基础
哈希表的内部实现原理;哈希函数;哈希碰撞;以及常见哈希表的区别;数组;set 和map。
什么时候想到用哈希法--------当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。 这句话很重要,大家在做哈希表题目都要思考这句话。
文章讲解:代码随想录
哈希表 Hash table
数组就是一张哈希表, 哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素。
一般哈希表都是用来快速判断一个元素是否出现集合里。
枚举复杂度O(n),哈希O(1)。
初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。将学生姓名映射到哈希表上就涉及hash function,也就是哈希函数。
有点怪,不知道为什么hashFuction又是hashCode。
哈希碰撞
映射到了索引下标 1 的位置,这一现象叫做哈希碰撞
拉链法
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了。
(数据规模是dataSize, 哈希表的大小为tableSize)
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
线性探测法,一定要保证tableSize大于dataSize
依靠哈希表中的空位来解决碰撞问题。
冲突的位置,放了小李,那么就向下找一个空位放置小王的信息
常见的三种哈希结构
- 数组
- set (集合)
- map(映射)
- 虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
-
功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。
242.有效的字母异位词数组 用来做哈希表 给我们带来的遍历之处。
题目链接/文章讲解/视频讲解: 代码随想录
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
// 整数数组 record,其大小为26,并将所有元素初始化为0。
// 在C或C++中,数组的元素默认不会被初始化,除非你明确地给它们赋值。
// 求相对值,记录s用+,对照t用-,最后确定是否对仗
for(int i = 0;i<s.size();i++){
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){
return false;
}
}
return true;
}
};
349. 两个数组的交集
建议:本题就开始考虑 什么时候用set 什么时候用数组,本题其实是使用set的好题,但是后来力扣改了题目描述和 测试用例,添加了 0 <= nums1[i], nums2[i] <= 1000 条件,所以使用数组也可以了,不过建议大家忽略这个条件。 尝试去使用set。
题目链接/文章讲解/视频讲解:代码随想录
输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序
使用数组来做哈希的题目,是因为题目都限制了数值的大小。
而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。
而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
此时就要使用另一种结构体了,set ,关于set,C++ 给提供了如下三种可用的数据结构:
- std::set
- std::multiset
- std::unordered_set
std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set;// 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(),nums1.end());// 存放nums1,给nums1去重
for(int num:nums2){
// nums_set也就是nums1,能不能找到nums2的每个元素
if(nums_set.find(num) != nums_set.end() ){
// nums_set.find(num): 这是调用 std::set 的 find 方法。它会尝试在 nums_set 中查找一个值,该值等于 num。如果找到,它会返回一个指向该元素的迭代器。如果没有找到,它会返回 nums_set.end(),这是一个指向集合结束位置之后的迭代器。
result_set.insert(num);
//会将 num 插入到 result_set
}
}
return vector<int>(result_set.begin(),result_set.end());
// begin()和end()方法分别返回指向容器中第一个元素的迭代器和指向容器末尾之后位置的迭代器。两个合在一起就是遍历
}
};
class Solution {
public:
int getsum(int n){
// 这里算这个数字是不是快乐的
int sum = 0;
while(n){
// 通过每次对10求余,求雨是百分号
sum += (n%10)*(n%10);
// 算下一个十位数
n = n/10;
}
return sum;
}
bool isHappy(int n) {
int sum;
// 要用set的原因是为了,防止求快乐数过程中,产生了无限循环,所以用set来保存,每次求出来的快乐数
unordered_set<int> unordered_set;
while(1){
// 没有跳出条件就用1
sum = getsum(n);
if (sum == 1){
return true;
}
if(unordered_set.find(sum) != unordered_set.end() ){
// 找到了就代表循环了
return false;
}else{
unordered_set.insert(sum);
}
n = sum;
}
}
};
- 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
- set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
- map ,map是一种key value的存储结构,可以用key保存数值,用value再保存数值所在的下标。
这道题目中并不需要key有序,选择std::unordered_map 效率更高!
- map用来做什么
map目的用来存放我们访问过的元素,
- map中key和value分别表示什么
map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> map;
// map的每个元素都是一个std::pair<const int, int>
// 第一个元素(first),第二个元素(second)
for(int i = 0;i < nums.size();i++){
// auto是一个关键字,用于自动类型推导。当你声明一个变量时,如果你不想明确指定变量的类型,而是让编译器根据该变量的初始化来自动决定其类型,你可以使用auto。
// map.find 是在找some key,所以只在first里边找
// find方法专门用于查找键(即first部分)。它不考虑关联的值(即second部分)。
auto paar = map.find(target - nums[i]);
if(paar!=map.end()){
return {paar -> second,i};
// paar -> second paar的序数
// i
}
map.insert(pair<int,int>(nums[i],i));
}
return {};
}
};