大家好,新的一周开始了,过去的数组与链表不知道大家掌握的如何?今天我们就要开始哈希表了,如果大家觉得没有掌握扎实数组与链表一定要回去好好复习一下,因为接下来的哈希表有些操作还是会基于数组与链表的基础知识,因此大家一定要把基础打好。那么废话不多说,我们直接开始今天的主题——哈希表。
第一部分哈希表理论基础
既然我们要逐渐使用哈希表解决问题,我们就要先来了解哈希表的理论基础,首先大家可以通俗地认为哈希表就是一个数组,那哈希表常用来解决什么问题呢?代码随想录网站上有说明哈希表主要是查找一个元素在不在数组里,那有的朋友可能就很疑惑,我数组不也可以实现吗?对的,数组的确可以实现查找一个元素在不在这个数组里,但其实我用数组的时间复杂度是O(n), 如果使用哈希表那我的时间复杂度就是O(1),哈希表其实是可以优化我们的算法的,例如要查询一个名字是否在这所学校里。我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。其实这里就会用到哈希函数,其实我再给大家扩充一下,就是我们一半是可以保证一一映射的,就是一个元素只会对应一个下标,但是如果有多个元素映射到一个下标呢?那其实就产生了哈希碰撞,其实我们这时候就可以将发生冲突的元素都被存储在链表中,还有使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。就是把冲突的元素找一个空的位置存下就可以。那还有要给大家说明的就是一些常用的哈希结构,这里代码随想录都给大家总结了,我也放在这里大家看看,做题的时候不一定哪一种结构就是最合适的,因此大家要多积累,对各种哈希结构都要有明确的了解。

当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。同样我们使用map的时候也是unordered_map使用比较多。
第一题有效的字母异位词对应力扣上编号为242的题目
我们要知道什么是字母异位词,就是我每个字母出现的次数都是一样的,总长也是相等的,只不过字母出现的顺序会变而已,这叫字母异位词,这个我们首先要了解,那我们就是用我们的哈希结构来解决一下这道题目:

那么拿到题目呢其实我有思路我肯定要用map来存储,key来存储具体的字母,value来存储每一个字母出现的次数,其实题目没有要求必须有序,所以我们应该是可以使用unordered_map来解决,但是可以有更好的方法,就是我先使用一个数组来存储a到z每一个字母出现的次数,其实数组的下标就可以代表具体的字母,比如说s字符串里有一个c,我如何记录呢?hash[s[i] - 'a']++,就可以这样就记录下来了,那这样这个数组下标是2的位置就是记录了c在这个字符串出现的次数,那我应该如何判断两个字符串是不是有效的字母异位词呢?其实思路不难想就是我再遍历一遍另一个字符串,比如我又遇到了c我就hash[t[i] - 'a']--,我就减1,那这样如果是有效的字母异位词我最终的这个数组应该都是0,最后我再遍历一遍这个数组如果发现了不是0的元素说明两个字符串存在出现次数不一样的字母那就不是有效的字母异位词,如果都是0,那就是了,那么我们一起来看一下代码是如何写的:
class Solution {
public:
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;
}
};
一共就只有26个字母,所以我数组开的足够了,具体思路上面我讲的很清楚了,包括如何统计每一个字母出现的次数,最后遍历数组如果发现有不是0的那就直接判定为不是,如果都是0那就判定为是。本题难度不大,相信大家可以理解我的思路。
第二题两个数组的交集对应力扣编号为349的题目
接下来来到我们今天的第二题,数组的交集这个好说就是找出在两个数组里都出现过的元素就是了,我们来看一下题目要求:

题目说了输出结果中的每个元素一定是唯一的,这个其实我们很自然就会想到要使用set了,因为set有去重的功能,但究竟用哪一种set最好呢?其实后面题目又说了不考虑输出结果的顺序,这样基本上我们就可以选择我们要使用的哈希结构就是unordered_set了,那究竟如何操作,我又两种方法可以解决,我先来介绍第一种,就是使用unordered_set,首先结果要存在一个unordered_set里面,同时我们可以将第一个nums1转成unordered_set,这样可以遍历的元素少一些快一些,因为其实保留相同的也没用以后输出结果也会过滤掉,那样我们就去遍历nums2里面的元素,如果发现在nums1转化成的unordered_set里面,那就说明这是一个交集里面的元素我就应该插入到结果里面,最后我再把结果转为vector返回就可以,我们来看一下这种思路的代码:
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());
}
};
很明显,大家重点要看一下语法,如何转为vector与unordered_set,这点要注意,其实思路不难,还有一种思路,也是得用unordered_set,其实我还是用上一个题目的思路,使用一个数组,如果出现在nums1中就将数组中以这一个元素值为索引的值永远变为1,那在接下来再去遍历nums2,如果发现nums2对应元素这里的数组的值是1,那就说明出现过,就将这个nums2里面的元素添加到result集合里面去,最后记得要将这个集合转为vector返回,我们来看一下这一种思路的代码该如何写:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
int hash[1005] = {0};//由于数据范围小了数组吃的消了
std::unordered_set<int> result;
vector<int> result;
for (int i = 0; i < nums1.size(); ++i)
{
hash[nums1[i]] = 1;
}
for (int i = 0; i < nums2.size(); ++i)
{
if (hash[nums2[i]] == 1)
{
result.push_back(nums2[i]);
}
}
return vector(result.begin(), result.end());
}
};
其实思路明白了看代码就很简单了,还是得注意一下语法,不要写错,这样这道题相信大家也可以明白,这道题就讲解到这里。
第三题快乐数对应力扣上编号为202的题目
这个题目有点思维量了,如何找到快乐数呢?首先我们要先知道快乐数是什么,它满足什么条件,我们直接来看题目:

题目说的很明白,快乐数就是各个数位上的数的平方和相加如果最后能变为1就是快乐数,但是注意我们每一次都要更新的,就是求完平方和得到了一个数,再用这个新得到的数再求平方和,是这样一直重复下去如果可以变为1就可以是快乐数,那究竟该如何判断是不是快乐数呢?我们首先要先求出一个数的每个数位的数的平方和,因此我们要编写一个函数来求,我就是%10与/10持续运算,只要这数还满足大于0,但是%10是取出当前的最低位,记得要平方,最后可以求出平方和,接下来就是看是不是快乐数,其实思路也很简单,但有点不好想,就是大家可以想想如果我平方和求了半天又回来了那么肯定凑不出1了,因为一旦回来又会重新走一遍,永远得不到1,而且想必大家都知道1的平方是1,这样的数就绝对不会是快乐数,其他的情况都会是快乐数,就是多算几次最后都会到1,因此我们可以使用一个unordered_set来存储每一次求出来的平方和,如果新求出来的平方和可以在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) {
std::unordered_set<int> result;
while(1)
{
int sum = getSum(n);
if (sum == 1) return true;
//如果找到了说明平方和又回来了要是可以变成1早就return true了
if (result.find(sum) != result.end()) return false;
else result.insert(sum);
n = sum;//注意这里一定要赋值
}
}
};
这里就不多说了,上面其实解释的很详细了,大家还是多注意语法,一定保证语法正确,同时不要忘记赋值,还要学会如何取出一个数字上每一个数位上的数字。本题我们就看到这里,接下来进入下一道题。
第四题两数之和对应力扣上编号为1的题目
来到今天的最后一题,这里是很多朋友梦开始的地方,虽说这是力扣的第一题,但其实难度对于新手来说也是有的,大家不要小看第一题,一定要认真思考,这个题其实可以使用暴力的但是时间复杂度是O(n^2),我们先使用暴力来解一下,首先来看一下题目要求:

题目要求我们返回数组中两个元素的和是target的两个元素的下标,其实这个暴力想法不难,但是题目要求了不能使用两次相同的元素,因此我们可以这样暴力:
class Solution {
public:
vector<int> twoSum(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)
{
return {i,j};
}
}
}
return {};
}
};
注意我的循环是如何保证两个元素不一样的,还有注意我们返回的形式是vector这个要注意,注意我们返回时的语法,如果没找到就返回空,这个暴力思路的代码是比较简单的,大家应该很快就可以理解,但我们接下来的思路其实更奇妙,既然我们今天的主题是哈希,那么我们一定是可以通过哈希结构解决这道题,那我们应该如何做?
很明显我们应该选用map,key是元素,value是下标,这点必须要注意,那究竟使用哪一种map呢?其实只要题目没有顺序要求一般我们使用unordered_map,这里我们就可以使用,因为题目说了我们可以使用任意顺序输出答案,那么我们看一下具体思路,首先就是我们先遍历数组,我们要查找的目标值其实就是target - nums[i],如果可以在map中找到就是有这样的两个元素,这样就可以返回题目要求的vector形式,如果没有找到就将当前元素的值与索引存放到map里面因为以后可能会有的,也是因为这种情况我们的map里面才有了元素,但是这种写法大家对语法可能比较陌生,大家仔细看一下代码,思路其实不太不难,但是语法可能比较容易出错:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> mp;
for (int i = 0; i < nums.size(); ++i)
{
auto iter = mp.find(target - nums[i]);
if (iter != mp.end())
{
return {iter -> second, i};
}
//如果目前没找到要将其添加到映射里去因为以后可能会有
mp.insert(pair<int,int>(nums[i], i));
}
return {};
}
};
大家仔细看我们用到了C++里面的STL,这个注意使用到了pair来将这时候的元素与下标存在map里面,还有auto这个语法大家可能不熟,其实在算法竞赛里面是很常用的,这里的iter其实可以理解为是一个二元组,前面是元素值,后面是下标,我们要返回下标,因此我们写iter->second,其实pair也是这种语法,大家可以去搜一下,这道题目就给大家讲解这么多。
总结今天的哈希表
其实今天的题目不算难,明天的三数之和就有难度了,大家把今天的笑话好,今天我主要是复习以前学的,原来就接触过哈希表了,大家多想,要考虑清楚各种哈希结构的区别与联系,在解题时要有清晰的思路,可以想到使用哪一种结构是最合适的,今天的文章就写到这里了,感谢大家观看!

243

被折叠的 条评论
为什么被折叠?



