今日主要内容:哈希表基础(哈希表理论基础、242.有效的字母异位词、 349. 两个数组的交集、202. 快乐数、1. 两数之和)
前置准备
1、哈希表基础
鸡毛不懂,学习一下。
based on C++ 学习依据代码随想录:)
大佬指导说应该把每个题的链接挂起来XD,从这次开始,周末复习的时候补上之前的。
内容学习
哈希表理论基础
哈希表
哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表
哈希表是根据关键码的值而直接进行访问的数据结构。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
如果hashCode得到的数值大于哈希表的大小了,也就是大于tableSize了,此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。
此时问题又来了,哈希表我们刚刚说过,就是一个数组。如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。
接下来哈希碰撞登场
哈希碰撞
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
哈希碰撞解决法——拉链法
发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
(数据规模是dataSize, 哈希表的大小为tableSize)
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
哈希碰撞解决法——线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放冲突的数据了。如图所示:
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
这里数组就没啥可说的了,我们来看一下set。
在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。
那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
哈希表总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!
我总结一下,看完之后好像知道hash表是什么了,除此之外好像什么都不知道。现在不仅对set、map不太懂,它的语法也没有一点掌握。感觉现在理解就是hash表就是一种思想?(?)然后底层的实现可以依赖数组啊、集合set啊、映射map。(这个地方纯纯在胡扯感觉,再找点资料研学一下)
哈希表可以将其比喻为一个大抽屉,抽屉里面有很多小格子。每个格子可以用来存放一些东西。
- 抽屉编号: 抽屉有编号,这个编号就是数据的
key
,我们通过这个key
来找到对应的抽屉。- 散列函数: 哈希表使用一种特殊的函数(哈希函数),来决定数据应该放在哪个抽屉里。这个函数将数据的名字
key
转换成一个数字,然后根据这个数字来选择一个抽屉。- 抽屉里的物品: 在每个抽屉里,可以放一些东西,这些东西就是我们要存储的数据。
- 解决冲突: 有时候不同的
key
经过散列函数后可能会得到相同的编号,这就是冲突。哈希表有方法来处理这些冲突。- 快速查找: 当我们需要找到某个数据时,哈希表可以通过名字
key
快速地找到对应的抽屉,然后取出里面的数据,这个操作非常快速,就像从抽屉中拿出东西一样。
题目1:242.有效的字母异位词
力扣题目链接:242. 有效的字母异位词 - 力扣(LeetCode)
给定两个字符串 s
和 t
,编写一个函数来判断 t
是否是 s
的字母异位词。
注意:若 s
和 t
中每个字符出现的次数都相同,则称 s
和 t
互为字母异位词。
示例 1:
输入: s = "anagram", t = "nagaram" 输出: true
愚蠢的尝试(该部分都是杂乱想法,不一定正确)
什么 意思呢?我好像都不知道应该要干嘛,乐,再回头复习一下。
呜 现在的理解就是,使用hash表容纳各个字符串,然后计数?相等就返回?但是具体要怎么操作呢?就看这个题先学习一下吧。
视频学习
哎这么回事,有点像数学刚学完理论就做应用题的感觉。相当于是把字母序列抽象成数列的下标了,然后用ACSII码来把前后顺序关系转变为下标的递进关系,感觉也太难想了:(
尝试写一下代码:有思路之后还是比较好写,但是想到感觉挺困难的= =,看看下一个吧
class Solution {
public:
bool isAnagram(string s, string t) {
int record [26] = {0}; 定义一个数组,用来承载26个字母
//int record [26];
for (int i=0;i<s.size();i++){ 在s串里,第i个位置的字母和’a'差多少,就在数组的第几位+1
record[s[i]-'a']++;
}
for (int i=0;i<t.size();i++){ 在t串里,第i个位置的字母和’a'差多少,就在数组的第几位-1
record[t[i]-'a']--;
}
for (int i=0;i<26;i++){
if( record[i]!=0){
return false;
}
}
return true;
}
};
题目2:349. 两个数组的交集
力扣题目链接:349. 两个数组的交集 - 力扣(LeetCode)
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2]
愚蠢的尝试(该部分都是杂乱想法,不一定正确)
想着沿用第一题的方法,nums1进来一个数就给他这个数的对应的下标+1,如果已经为1就不行动,然后再用nums2进来数,全部都+1,最后把值为2的结果输出。但之前看的时候好象说就是大数字就用别的结构来着。学视频
视频学习
难绷,我好像从没有掌握过一些语法语句
创建一个集合的写法如下
// 创建一个存储整数的无序集合
unordered_set<int> mySet;
想要向集合中插入元素需要使用insert()
方法
// 向集合中插入元素
mySet.insert(1);
想要往集合中删除元素需要使用erase
方法
mySet.erase(1);
find()
方法用于查找特定元素是否存在于集合中,如果 find()
方法找到了要查找的元素,它会返回指向该元素的迭代器,如果未找到要查找的元素,它会返回一个指向集合的 end()
的迭代器,表示未找到。通过比较find()
方法返回的迭代器是否等于 end()
,可以确定集合中是否有查找的元素。
// 判断元素是否在集合中, 只要不等于end(), 说明元素在集合中
if (mySet.find(i) != mySet.end()) {
}
范围for循环
C++11引入了范围for循环,用于更方便地遍历容器中的元素。这种循环提供了一种简单的方式来迭代容器中的每个元素,而不需要显式地使用迭代器或索引。
for (类型 变量名 : 容器) {
// 在这里使用一个变量名,表示容器中的每个元素
}
比如下面的代码就表示使用范围for循环遍历一个容器
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用范围for循环遍历向量中的元素
for (int num : numbers) {
std::cout << num << " ";
}
范围for循环不会修改容器中的元素,它只用于读取元素。如果需要修改容器中的元素,需要使用传统的for循环或其他迭代方式。
此外,还可以使用auto
关键字来让编译器自动推断元素的类型,这样代码会更通用
// 使用auto关键字自动推断元素的类型
for (auto num : numbers) {
std::cout << num << " ";
}
以上基础知识补充一下。回顾一下题目的思路:-)
也即首先建立一个unorderset数组,用于处理数组的重复问题。之后将nums1内的数据放入到numset中存储。之后读取nums2内的数据,如果有和nums1的数据相同的,就插入到结果数组中。代码如下:
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());
}
};
实际上修改过之后还是使用数组来处理比较好:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
int hash[1005] = {0}; // 默认数值为0
for (int num : nums1) { // nums1中出现的字母在hash数组中做记录
hash[num] = 1;
}
for (int num : nums2) { // nums2中出现话,result记录
if (hash[num] == 1) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
题目3: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
愚蠢的尝试(该部分都是杂乱想法,不一定正确)
这里考虑使用一个数组,通过取余获得各个位置的数,通过insert方法插入数组中,对每个位置进行平方加和,如果加和为1,即返回true。否则继续循环。(但是这里如果无限循环怎么办呢)看看视频怎么说。
视频学习(这个没视频,鉴定为答案学习)
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!这里也是把sum不断地存入集合中。
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
判断sum是否重复出现就可以使用unordered_set。这就是判断无限循环的方法。
class Solution {
public:
int getSum(int a){
int num = 0;
while(a){
num += (a % 10) * (a % 10);
a /= 10;
} 求每一位上值的平方和
return num;
}
bool isHappy(int n) {
unordered_set<int> set; 定义set集合
while(1) {
int sum = getSum(n); 计算sum,如果为1就返回true,如果不为1,则和之前的比较,如果之前已经出现过了,那么就说明陷入循环,返回false,如果没出现,则插入到集合中
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
有个小疑问就是,比如出现18和81这样sum相等但是数字不等的情况,如何处理呢。
题目4 :1. 两数之和
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
愚蠢的尝试(该部分都是杂乱想法,不一定正确)
没啥思路,目前想的就是使用两层for循环遍历每个数,如果加和为target就保存其下标insert到集合内,如果insert的数值不相同,则返回下标?
视频学习
这里视频里的思路是:当遍历到一个数时,直接计算他的(target-数)的结果,并检查对应的数是否在数组中,如果不在,要把当前遍历到的数字放入集合中,当继续向后遍历时,就继续在集合中检查相应元素。所以,拿数组里的值作为key,拿数组值对应的下标作为value。
本题其实有四个重点:
为什么会想到用哈希表
哈希表为什么用map
本题map是用来存什么的
map中的key和value用来存什么的
把这四点想清楚了,本题才算是理解透彻了。
很多人把这道题目通过了,但都没想清楚map是用来做什么的,以至于对代码的理解其实是 一知半解的。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int,int> map; 定义map
for (int i=0;i<nums.size();i++){ for循环
int cha = target-nums[i];
auto iter = map.find(cha); 自动查找等于cha的值
if(iter != map.end()){
return {iter->second,i};
}
map.insert(pair<int,int>(nums[i],i)); 如果没找到,把当前数放到map中
}
return {};
}
};
补充一个代码知识
map<string,vector<string>> M;
auto it = M.begin();
第一行声明一个图M,string是这个图的key,vector<string>是其value;
第二行把M的第一个元素赋给it。
it 表示的是图M的第一整个元素;
it->first 表示的是这个元素的key的值;
it->second 表示的是这个元素的value的值。
(it+1)可以用来表示下一个元素,这可以用在循环中,遍历图:
for(auto it = M.begin();it!=M.end();it++){ }
ps:这种用法在map和unordered_map中都要用到(需要注意的是,map中储存是按照压入顺序放置的,而unordered_map中储存是乱序。
总结
断断续续的学完了这几题,只能说感官上质量很低,主要还是不熟练相关的语法操作,导致感官上就难度很大,畏难了就。但是想想这才哪到哪,被这点困难就击毙了也太逊了啦。