代码随想录算法训练营第六天||242.有效的字母异位词、349.两个数组的交集 、202.快乐数、1.两数之和
一、哈希表理论基础
哈希表
- 一般哈希表都是用来快速判断一个元素是否出现集合里
哈希函数
哈希函数,把值直接映射为哈希表上的索引,然后就可以通过查询索引下标快速找到对应的值。
哈希碰撞
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
拉链法
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
常见的三种哈希结构
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
- 数组
- set (集合)
- map(映射)
这里数组就没啥可说的了,我们来看一下set。
在C++中,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的存储方式使用红黑树实现的。
其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。
虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,std::set、std::multiset 使用红黑树来索引和存储,不过给我们的使用方式,还是哈希法的使用方式,即key和value。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。
这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢?
实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。
二、242有效的字母异位词
数组就是简单的哈希表,但是数组的大小可不是无限开辟的,这题对应的值不多,可以开辟大小为26的数组来存储字符出现的次数,然后索引则用字符a到字符z的ASCII表的相对位置表示。
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
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数据类型的哈希表应用,C++语法还没学完做这道题有点吃力,只能边学边用了,看了carl哥的代码才写的出来,明天需要再刷一遍。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result;
unordered_set<int> num_set(nums1.begin(), nums1.end());
for(int i = 0; i < nums2.size(); i++){
if(num_set.find(nums2[i]) != num_set.end())
result.insert(nums2[i]);
}
return vector<int>(result.begin(),result.end());
}
};
四、202.快乐数
这道题仍然是采用set数据类型的一道题,本题一刷通过,一刷思路如下:循环每次判断新得到的平方和是不是set里已经有的数,如果没有就把改数组加入set里,一直循环直到出现平方和为1或者新得到的平方和已经在set里了。代码如下:
class Solution {
public:
bool isHappy(int n) {
unordered_set<int> num_set;
int sum = n;
while(num_set.find(sum) == num_set.end()){
num_set.insert(sum);
int length = get_length(sum);
int temp = 0;
while(length--){
temp += (sum % 10) * (sum % 10);
sum = sum/10;
}
sum = temp;
if(sum == 1) return true;
}
return false;
}
int get_length(int x){
int length = 1;
while(x /10 != 0){
x = x/10;
length++;
}
return length;
}
};
解题思路大体上是对的,但是求和的操作有点笨拙所花的时间更多,修改后的代码:
class Solution {
public:
bool isHappy(int n) {
unordered_set<int> num_set;
int sum = n;
while(num_set.find(sum) == num_set.end()){
num_set.insert(sum);
sum = get_num(sum);
if(sum == 1) return true;
}
return false;
}
int get_num(int x){
int sum = 0;
while(x){
sum += (x % 10) * (x % 10);
x /= 10;
}
return sum;
}
};
五、1.两数之和
这题思路暴力解法比较简单,写两个for循环遍历一下就行了,复杂度为O(n^2)。由于题目要求返回对应的数组下标,因此考虑采用map的数组形式,但对map数据结构不太熟悉,直接看的解析。
通过map存储已经遍历过的值,然后判断map里是否存在target于当前遍历的值的差值,如果存在返回map对应key的value,不存在将当前遍历的值存入map。代码如下:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map<int,int> number;
for(int i = 0; i < nums.size(); i++){
auto iter = number.find(target - nums[i]);
if(iter != number.end()){
return {iter->second,i};
}
number.insert(pair<int,int>(nums[i],i));
}
return {};
}
};
六、总结
今天学习的内容是新的一个数据结构,题目都不是很难,但是对哈希表的操作上不够了解,明天二刷一遍加深印象和了解。抓紧时间把C++语法书跟上!