资料来源:代码随想录
1.哈希表的理论基础
哈希表是根据关键码的值而直接进行访问的数据结构。
比如:数组,关键码就是数组的索引下标。
一般哈希表都是用来快速判断一个元素是否在集合里/一个元素是否出现过。
哈希函数:把其他数据格式转化为不同的数值,映射为哈希表上的索引数字。
哈希碰撞:不同的数值映射到同一个索引下标的位置。
解决方法:
(1)拉链法:发生冲突的元素被存储到链表中。
(2)线性探测法:需要保证tablesize>datasize,用空位来存放冲突的元素。
三种常见的哈希结构:数组、set(集合)、map(映射)。
注意:使用数组来做哈希题目时,需要题目限制数值的大小。
set有三种可用的数据结构:
- std::set
- std::multiset
- std::unordered_set
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
2.有效的字母异位词 242
定义一个record数组(哈希表)用来记录字符串s里字符出现的次数,均初始化为0。
把字符映射到哈希表上。最多有26个字符,所以tablesize=26即可,即哈希表的索引下标从0-25.
在遍历字符串s的时候,只要将s中各元素对应的哈希表中的值+1即可;在遍历字符串t的时候,再将t中各元素对应的哈希表中的值-1.
最后检查:record数组中如果有元素不为0,说明s和t一定是谁多或谁少了字符,return FALSE;元素均为0的话,说明是字母异位词,return TRUE。
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26]={0}; //初始化一个有26个空位的数组,初值为0
for(int i=0; i<s.size(); i++)
{
record[s[i]-'a']++; //不用ASCII码,直接用相对位置即可
}
for(int j=0; j<t.size(); j++)
{
record[t[j]-'a']--;
}
//开始检查record
for(int i=0; i<26; i++)
{
if(record[i]!=0) //只要有一位数字不为0,就不是异位词
{
return false;
}
}
//每一位都为0
return true;
}
};
3.两个数组的交集 349
学会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; //定义一个无序集合来存放结果,这样可以给结果去重,满足“输出结果每个数都唯一”的要求
unordered_set<int> nums_set(nums1.begin(),nums1.end());
//把数组nums1中的元素存放进一个无序集合num_set里,给数据去重
for(int num : nums2) //冒号前是实例化一个集合中包含的元素,冒号后是要遍历的集合
//意思是遍历nums2集合中的每一个元素
{
if(nums_set.find(num)!=nums_set.end())
//在nums_set集合中寻找nums2集合中的num元素,返回的结果不是nums_set的末尾
//说明在nums_set集合中找到了这个元素
//如果是末尾,说明走了一遍都没找到这个元素
{
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;
int hash[1000]={0}; //定义一个数组,数组长度最大为1000
for(int num : nums1)
{
hash[num]=1;
}
//如果一个num在nums1中出现,在哈希表中把对应位置的数改成1
for(int num : nums2)
{
if(hash[num]==1)
{
result_set.insert(num);
}
//如果nums2中的元素num在哈希表中对应位置的数为1,说明这个元素在nums1中也出现过
//即为相交的元素,放入结果集合
}
return vector<int>(result_set.begin(),result_set.end());
}
};
4.快乐数 202
class Solution {
public:
int getSum(int n)
{
int sum=0;
while(n) //一直循环
{
sum=sum+(n%10)*(n%10);
n=n/10; //先是个位平方,然后是十位,再然后是百位...
}
return sum;
}
bool isHappy(int n)
{
unordered_set<int> set; //放结果
while(1)
{
int sum=getSum(n); //从上面接收一下计算好的sum
if(sum==1)
{
return true;
}
if(set.find(sum)!=set.end())
//能找到sum这个数,说明这个数之前出现过,所以已经出现循环了
{
return false;
}
else
{
set.insert(sum);
}
n=sum; //继续下一轮计算
}
}
};
5.两数之和 1
因为在本地,我们不仅要知道元素有没有遍历过,还有知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
再来看一下使用数组和set来做哈希法的局限。
- 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
- set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value在保存数值所在的下标。
这道题目中并不需要key有序,选择std::unordered_map 效率更高!
思路:
在遍历数组的时候,向map查询是否有和当前遍历元素所匹配的数值(即相加等于target),如果map中有,就找到匹配对,返回下标值;如果没有,就把当前遍历元素以(key,value)即(数值,下标)的方式存进map,因为map存放的就是我们访问过的元素。
一些其它的知识:
map<X,Y>里存放的实际上是一串pair<const X,Y>
如果用一个指针it来指向map中的元素:auto it=map.begin( ); 那么解引用*it将得到map中的第一个元素pair< >。
然后可以再接收pair中的两个元素:
it->first将得到key值,it->second将得到value值。
auto在这里起到自动类型识别的作用。用它来定义一个指针,就不用再去想指针是什么类型的了。
迭代器.find( ),如果没有找到的话,指向的是末尾元素的下一个,即.end( )。迭代器的使用方法和指针类似。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//先初始化一个map用来存放已经访问过的元素
std::unordered_map<int,int> map;
for(int i=0; i<nums.size(); i++)
{
//遍历数组元素,并在map中寻找是否有和当前元素匹配的值(即相加=target)
auto iter=map.find(target-nums[i]);
//定义一个自动识别类型的指针,指向可能的匹配的值
if(iter!=map.end()) //找到了
{
return {iter->second,i}; //则返回匹配的下标和数组中当前元素的下标
}
//没找到:将当前元素存入map,因为map用来存放我们已经访问过的元素
map.insert(pair<int,int>(nums[i],i));
}
return {};
}
};
本题的四个重点:
(1)为什么会想到用哈希表?
因为我们要在数组中寻找是否有和当前元素匹配的值,即查询一个值是否在数组中出现过,所以要用哈希表。
(2)哈希表为什么用map?
用数组的话,如果数组元素少而哈希表太大,就会造成内存空间的浪费;而set是一个集合,里面只能存放key值,而我们既需要记录元素的值,又需要记录元素的下标,所以用map。
(3)本题map是用来存什么的?
存我们已经访问过的元素的值和下标。
(4)map中的key和value用来存什么?
key用来存元素的值,value用来存元素的下标。
6.四数相加 454
【Leetcode】map[key]++的理解与用法_map[]++_fighting_!的博客-CSDN博客
在unorder_map中,可以使用方括号访问键对应的值map[key] ,map[key]是这个key值对应的value。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。
所以我们可以通过map[key]++来直接创建key且使key对应的value值+1,该句代码可以用在需要计数的情况下。
那么会产生一个问题(是我个人在第一次接触时所产生的问题):用map[key]++可以创建key,那么用map[key]--是不是可以删除key呢?
经验证:不是。通过++来建立键,但不可以通过--来删除键,删除只能试用erase函数来删除键。
思路:我们只需要得到满足条件的元组的数量即可,不需要得到具体的元组值。
首先遍历A和B两个数组,统计两个数组元素之和(作为key值)和出现的次数(作为value值),放到unordered_map中,对结果进行去重。
然后遍历C和D两个数组,在刚刚的map中寻找是否有0-(c+d)这个值,即为满足a+b+c+d=0这个要求的值。如果有的话,说明找到了符合要求的元组,将map中0-(c+d)这个值对应的value赋给count,统计出符合要求的元组的数量。
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> map;
//定义一个map来存放nums1和nums2遍历的结果
//key存放a+b的值,value存放a+b出现的次数
for(int a : nums1)
{
for(int b : nums2)
{
map[a+b]++; //计算a+b的值作为key,并将对应的value加1,。注意结果已经去重
}
}
int count=0; //统计a+b+c+d=0出现的次数
for(int c : nums3)
{
for(int d : nums4)
{
if(map.find(0-(c+d))!=map.end()) //找到了符合a+b+c+d=0的元组
{
count=count+map[0-(c+d)]; //map[0-(c+d)]是这个值对应的value
}
}
}
return count;
}
};
7.赎金信 383
(1)暴力解法
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
for(int i=0; i<magazine.size(); i++)
{
for(int j=0; j<ransomNote.size(); j++)
{
if(magazine[i]==ransomNote[j])
{
ransomNote.erase(ransomNote.begin()+j);
//在ransom里找是否有和magazine一样的字符,有的话,删除它
}
}
}
if(ransomNote.size()==0) //ransom里的都删完了,说明可以由magazine组成ransom
{
return true;
}
return false;
//注意:迭代器用size()和length()的效果是一样的
}
};
(2)哈希解法
思路:维护一个长度为26的数组,在里面相对应的位置存放进magazine每个字符出现的次数,然后用ransomNote来验证这个数组中是否包含了ransomNote所需要的所有字符。
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26]={0};
if(ransomNote.size()>magazine.size())
{
return false;
}
for(int i=0; i<magazine.size(); i++)
{
record[magazine[i]-'a']++; //把magazine每个元素,在record数组中对应的位置加一
}
for(int j=0; j<ransomNote.size(); j++)
{
record[ransomNote[j]-'a']--; //把ransomNote每个元素,在record数组中对应的位置减一
//如果此时record中有数小于0,说明ransom里存在magazine里没有的元素
if(record[ransomNote[j]-'a']<0)
{
return false;
}
}
return true;
}
};
8.三数之和 15
双指针法。
首先将数组排序。然后定义下标:i从0的地方开始,left从i+1的地方开始,right定义在数组结尾的地方。在数组中找到abc使得a+b+c=0,其中a=nums[i] b=nums[left] c=nums[right]。
如何移动指针?如果nums[i]+nums[left]+nums[right]>0,说明三数之和大了,因为数组是排序的,所以right下标应该左移,这样才能让三数之和减小。
如果nums[i]+nums[left]+nums[right]<0,说明三数之和小了,就让left下标右移,直到left和right相遇,说明还是不够大,这时需要移动i。所以有一个关于i的for循环。
去重和双指针同时收缩没懂。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
//定义一个容器存放结果
vector<vector<int>> result;
sort(nums.begin(),nums.end());
for(int i=0; i<nums.size(); i++)
{
if(nums[i]>0)
{
return result;
}
//nums[i]是三个数里最小的一个了,如果最小的都大于0,那么和不可能为0,直接返回结果
//对a去重(没懂)
if(i>0 && nums[i]==nums[i-1])
{
continue;
}
//定义双指针
int left=i+1;
int right=nums.size()-1;
while(left<right) //到left=right还没有=0的话,就该让i加1进下一轮循环了
{
if(nums[i]+nums[left]+nums[right]>0)
{
right--;
}
else if(nums[i]+nums[left]+nums[right]<0)
{
left++;
}
else
{
result.push_back(vector<int>{nums[i],nums[left],nums[right]});
//对b和c去重(没懂)
while(right>left && nums[right]==nums[right-1])
{
right--;
}
while(right>left && nums[left]==nums[left+1])
{
left++;
}
//找到答案,双指针同时收缩(没懂)
right--;
left++;
}
}
}
return result;
}
};
9.四数之和 18
看不懂!!!!