哈希表理论基础
哈希理论基础
建议:大家要了解哈希表的内部实现原理,哈希函数,哈希碰撞,以及常见哈希表的区别,数组,set 和map。
什么时候想到用哈希法,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。 这句话很重要,大家在做哈希表题目都要思考这句话。
哈希表
首先什么是哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。
哈希表是根据关键码的值而直接进行访问的数据结构。
这么官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素。
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
哈希碰撞:拉链法、线性探测法
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
242.有效的字母异位词
242.有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
示例 1:
输入: s = “anagram”, t = “nagaram”
输出: true
示例 2:
输入: s = “rat”, t = “car”
输出: false
提示:
1 <= s.length, t.length <= 5 * 104
s 和 t 仅包含小写字母
进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
之前没做过
错误:创建vector错误
vector<int> a[26];
文心一言:
std::vector<int> myVector; // 默认构造函数,创建一个空的vector
std::vector<int> myVector(10); // 创建一个包含10个int元素的vector,所有元素初始化为0
std::vector<int> myVector2(10, 1); // 创建一个包含10个int元素的vector,所有元素初始化为1
std::vector<int> originalVector = {1, 2, 3, 4, 5};
std::vector<int> copiedVector(originalVector); // 使用originalVector初始化copiedVector
std::vector<int> myVector = {1, 2, 3, 4, 5}; // 列表初始化
这个改对了之后,通过了 其实还忘记初始化为0了
class Solution {
public:
bool isAnagram(string s, string t) {
vector<int> a(26,0);
for(auto s1:s){
a[s1-'a']++;
}
for(auto t1:t){
a[t1-'a']--;
}
for(int i=0;i<a.size();i++){
while(a[i]!=0){
return false;
}
}
return true;
}
};
349. 两个数组的交集
349. 两个数组的交集
给定两个数组 nums1 和 nums2 ,返回 它们的
交集
。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
unordered_set是无序不重复
set是有序不重复(是什么序 自然升序)
multiset是有序有重复
在C++中,std::set是一个关联容器,它存储的元素是唯一的,并且按照一定的顺序进行排序。std::set内部通常使用红黑树(Red-Black Tree)来实现,这是一种自平衡二叉查找树,能够确保元素以有序的方式存储,同时提供高效的插入、删除和查找操作。
std::set中元素的排序顺序默认是按照元素的升序排列的。如果std::set存储的是自定义类型(如用户定义的类或结构体),那么必须为该类型提供一个比较函数或者使该类型重载<运算符,以便std::set能够知道如何比较两个元素。
默认排序
对于内置类型(如int、double等),std::set直接使用这些类型的自然顺序(如数字的升序)来排序元素。
自定义类型排序
如果std::set包含自定义类型的元素,你需要定义如何比较这些元素。这可以通过两种方式实现:
重载<运算符:在你的类或者结构体内部重载<运算符,这样std::set就可以直接使用这个运算符来比较元素了。
struct MyStruct {
int id;
// ... 其他成员 ...
// 重载<运算符
bool operator<(const MyStruct& other) const {
return id < other.id;
}
};
std::set<MyStruct> mySet;
提供自定义比较函数或比较类:你可以在创建std::set时,提供一个自定义的比较函数或比较类(通过模板的第三个参数)。这种方式允许你更灵活地定义排序逻辑,即使不能或不想修改原有类型。
struct MyStruct {
int id;
// ... 其他成员 ...
};
// 自定义比较函数
bool compareMyStruct(const MyStruct& a, const MyStruct& b) {
return a.id < b.id;
}
// 或者使用比较类
struct CompareMyStruct {
bool operator()(const MyStruct& a, const MyStruct& b) const {
return a.id < b.id;
}
};
// 使用自定义比较函数
std::set<MyStruct, decltype(&compareMyStruct)> mySet1(compareMyStruct);
// 或者使用比较类
std::set<MyStruct, CompareMyStruct> mySet2;
注意:在上面的自定义比较函数示例中,std::set的模板参数需要一点技巧来正确指定比较函数的类型。通常,对于简单的函数,直接使用函数指针(如decltype(&compareMyStruct))可能不够直接,因为std::set期望的是一个可调用对象(如函数指针、函数对象或lambda表达式),且可能还需要额外的适配器(如std::function或std::less的特化)来确保类型正确。在实际应用中,使用比较类或lambda表达式作为模板参数更为常见和方便。
结论
std::set中的元素是按照一定顺序存储的,这个顺序默认是升序,但可以通过自定义比较函数或比较类来修改。对于自定义类型,必须提供比较逻辑,否则编译器无法知道如何比较两个元素。
第一次尝试
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
//输出元素唯一
unordered_set<int> seen;
vector<int> v;
for(auto n:nums1){
seen.insert(n);
}
for(auto n2:nums2){
if(seen.find(n2)!=seen.end()){
v.push_back(n2);
}
}
return v;
}
};
只考虑了数组1放进去的时候没有重复了,没考虑数组2找的时候可能会有重复
并且vector没有find函数
想到既然unordered_set可以去重就把他也放进一个seen里这样就没有重复了,虽然蛮好理解,但是有点怪怪的,通过了
第二次尝试
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
//输出元素唯一
unordered_set<int> seen;
unordered_set<int> seen2;
vector<int> v;
for(auto n:nums1){
seen.insert(n);
}
for(auto n2:nums2){
seen2.insert(n2);
}
for(auto n3:seen2){
if(seen.find(n3)!=seen.end()){
v.push_back(n3);
}
}
return v;
}
};
我的内存占很大
看看carl的
代码随想录
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());
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());
}
};
他也是用了两个unordered_set 只不过是一个num1 还有一个是结果数组
并且这两个方式需要注意:
把一个数组放进set里
unordered_set<int> nums_set(nums1.begin(), nums1.end());
把一个set转化成数组 还可以直接return
vector<int>(result_set.begin(), result_set.end());
关注可以用数组作为哈希结构的情况
202.快乐数
202.快乐数
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
示例 2:
输入:n = 2
输出:false
提示:
1 <= n <= 231 - 1
第一次尝试
写出来了 中间有点小问题 n=n/10 应该在int k=n%10的后面因为他重新设定了n的大小
class Solution {
public:
bool isHappy(int n) {
int count=0;
while(n!=1&&count<=10){
int k=0;
while(n!=0){
int b=n%10;
n=n/10;
k+=b*b;
}
n=k;
count++;
}
while(count>10){
return false;
}
return true;
}
};
但是我这里无限循环设置了十轮判定 不知道对不对
看了carl
代码随想录
哦哦原来会导致无限循环的原因是在求和的时候会出现重复的和导致循环
思路
这道题目看上去貌似一道数学问题,其实并不是!
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
正如:关于哈希表,你该了解这些! (opens new window)中所说,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。
所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
判断sum是否重复出现就可以使用unordered_set。
还有一个难点就是求和的过程,如果对取数值各个位上的单数操作不熟悉的话,做这道题也会比较艰难。
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.两数之和
1.两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案
这个之前做过
第一次尝试
class Solution {
public:
vector<int>twoSum(vector<int> &nums,int target)
{
unordered_map<int,int> map;//注意:键:数值 值:下标值
for(int i=0;i<nums.size();i++){
if(map.find(target-nums[i])!=map.end()){//如果找到了 就返回下标值
return {map[target-nums[i]],i};
}
map[nums[i]]=i;
}
}
};
不对 不知道这个return对不对 事实上是对的 return加一个{}
return {map[target-nums[i]],i};
注意:键:数值 值:下标值(关键点)
错误的是最后没加return{}
因为在编译的时候不知道你给的数据是否满足一定有两数之和等于target,所以要加上一个不满足时的输出,使编译成立
之前写的
//两数之和
//哈希表map<内容,下标> 在map里找target-数值 如果没有就把这个数值放进去
unordered_map <int,int> map;
for(int i=0;i<nums.size();i++)
{
auto it=map.find(target-nums[i]);//map找target-nums[i]
if(it!=map.end())//找到了
{
return {it->second,i};//返回该位置下标 和在map中找到的对应值的储存的下标
}
map.insert(pair<int,int>(nums[i],i));//把数字和下标存进map里
}
return {};
啥叫打字机模式? (?)