目录
基础知识
哈希表
哈希函数
2.哈希碰撞
常见的哈希结构(三种)
数组
集合set
映射map
经典题目
数组作为哈希表
例题:力扣242 已完成
例题:力扣383 已完成
例题:力扣49
例题:力扣438
set作为哈希表
例题:力扣349 已完成
例题:力扣202 已完成
例题:力扣350
map作为哈希表
例题:力扣1 已完成
例题:力扣454 已完成
双指针方法
例题:力扣15 已完成
例题:力扣18 已完成
遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
基础知识
哈希表
1.哈希表是根据关键码的值而直接进行访问的数据结构
2.哈希表都是用来快速判断一个元素是否出现集合里
eg.只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
将学生姓名映射到哈希表上就涉及到了hash function ,也就是哈希函数。
哈希函数
把学生姓名直接映射为哈希表上的索引,然后就可以查询索引下标快速直到这位同学是否再这所学校里了。
同哟hashcode把名字转化为数值,hashcode是通过特定编码方式,可将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
1.为了保证映射出来的索引数值都落在哈希表上,再次对数值做一个取模操作
2.哈希碰撞
解决几个同名的学生映射到哈希表的同一个位置
①拉链法:
小李1和小李2再索引1的位置发生了冲突,发生冲突的元素都被存储在链表中,这样就能通过索引找到小李1和小李2了。
*数据规模是dataSize,哈希表的大小为tableSize
#选取适当的哈希表的大小,既不会因为数组空值浪费大量内存,不会因为链表太长在查找上浪费太多时间
②线性探测法:
**tableSize大于dataSize,依靠哈希表中的空位解决碰撞问题
常见的哈希结构(三种)
数组
集合set
优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
1.std::set
底层实现:红黑树,key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加
2.std::multiset
底层实现:红黑树,key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加
3.std::unordered_set
底层实现:哈希表
映射map
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
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) |
经典题目
数组作为哈希表
例题:力扣242 已完成
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
class Solution {
public:
bool isAnagram(string s, string t) {
int hash[26];
//初始化为0
for(int i=0;i<26;i++){
hash[i]=0;
}
//分别求长度
int slen=s.length();
int tlen=t.length();
for(int i=0;i<slen;i++){
hash[s[i]-'a']++;
}
for(int j=0;j<tlen;j++){
hash[t[j]-'a']--;
}
//如果均为0,说明刚好是有效字母异位词,返回true;否则返回false
for(int i=0;i<26;i++){
if(hash[i]!=0)
return false;
}
return true;
}
};
思路:①题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
②定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。
③遍历 字符串s的时候,只需要将 s[i] - 'a' 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。
④同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
⑤record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。
例题:力扣383 已完成
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
/*暴力解法
for(int i=0;i<magazine.length();i++){
for(int j=0;j<ransomNote.length();j++){
if(ransomNote[j]==magazine[i]){
ransomNote.erase(ransomNote.begin()+j);
break;
}
}
}
if(ransomNote.length()==0){
return true;
}
return false;*/ //这里时间复杂度是比较高的,而且里面还有一个字符串删除也就是erase的操作,也是费时的
//哈希解法
//用一个长度为26的数组还记录magazine里字母出现的次数。然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
int hash[26];
//初始化
for(int i=0;i<26;i++){
hash[i]=0;
}
//如果更长,直接pass为false
if(ransomNote.length()>magazine.length()){
return false;
}
for(int i=0;i<magazine.length();i++){
// 通过hash数据记录 magazine里各个字符出现次数
hash[magazine[i]-'a']++;
}
for(int j=0;j<ransomNote.length();j++){
// 遍历ransomNote,在hash里对应的字符个数做--操作
hash[ransomNote[j]-'a']--;
if(hash[ransomNote[j]-'a']<0){ //如果小于零说明ransomNote里出现的字符,magazine没有
return false;
}
}
return true;
}
};
思路:
① 用一个长度为26的数组还记录magazine里字母出现的次数。
②用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。
例题:力扣49
思路:
例题:力扣438
思路:
set作为哈希表
例题:力扣349 已完成
给定两个数组
nums1
和nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
/*set的写法
unordered_set<int> res_set;//存放结果,用set的目的是去重
unordered_set<int> nums_set(nums1.begin(),nums1.end());//存放num1的数据
for(int num:nums2){
//发现nums2的元素在nums_set中出现过
if(nums_set.find(num)!=nums_set.end()){
res_set.insert(num);//插入
}
}
//返回结果
return vector<int>(res_set.begin(),res_set.end());*/
//数组的写法
unordered_set<int>res_set;//存放结果,用set对结果去重
int hash[1005]={0};//默认值为0
for(int num:nums1){ //nums1中出现的字母在hash中记录
hash[num]=1;
}
for(int num:nums2){//nums2中出现的话,res记录
if(hash[num]==1){
res_set.insert(num);
}
}
return vector<int>(res_set.begin(),res_set.end());
}
};
思路:unordered_set
①输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序。
②如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
例题:力扣202 已完成
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
class Solution {
public:
//取数值各个位上的单数之和
int getSum(int n){
int sum=0;
while(n){
sum+=(n%10)*(n%10);
n=n/10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> resset;
while(1){
int sum=getSum(n);
if(sum==1){
return true;
}
//检测这个数之前是否出现过
//如果这个数出现过,证明已经进入了循环,直接返回false
if(resset.find(sum)!=resset.end()){
return false;
}
else{
resset.insert(sum);//将sum数放进原来的集合里面,用下次判断
}
n=sum;//更新n的值
}
}
};
思路:
使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。
判断sum是否重复出现就可以使用unordered_set。
例题:力扣350
思路:
map作为哈希表
例题:力扣1 已完成
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//1.map用来存放遍历过的元素,在遍历数组时询问集合中是否出现过这个元素
//2.key用来存放元素,value用来存放下标
std::unordered_map<int,int>map;
for(int i=0;i<nums.size();i++){
//遍历当前元素,在map中寻找是否有匹配的key
auto iter=map.find(target-nums[i]);
if(iter!=map.end()){
return{iter->second,i};
}
//如果没有找到匹配对,九八访问过的元素和下标存在map中
map.insert(pair<int,int>(nums[i],i));
}
return{};//如果没找到就返回空
}
};
思路:
①map用来做什么:用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下表,这样才能找到与当前元素相匹配的
②map中的key和value分别表示什么:数组中的元素作为key,有key对应的就是value,value用来存下标
例题:力扣454 已完成
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> umap;key:a+b的数值,value:a+b数值出现的次数
for(int a:nums1){
for(int b:nums2){
umap[a+b]++;
}
}
int count=0;//统计a+b+c+d=0出现的次数
//遍历C和D,找到如果如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来
for(int c:nums3){
for(int d:nums4){
if(umap.find(0-(c+d))!=umap.end()){
count+=umap[0-(c+d)];
}
}
}
return count;
}
};
思路:
①定义unordered_map,key放ab的和,value放ab和出现的次数
②遍历大A和B数组,统计两个数组元素之和,和出现的次数,放到map中
③定义遍历count,统计a+b+c+d=0出现的次数
④遍历CD数组,如果0-(c+d)在map中出现过,用count把map中key对应的value统计出来
⑤返回统计值count
双指针方法
用来解决三数、四数之和问题
例题:力扣15 已完成
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
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;
}
// 正确去重a方法
if(i>0&&nums[i]==nums[i-1]){ //i>0保证数组不会越界
continue;
}
int left=i+1;//left在当前头的右边第一个
int right=nums.size()-1;//最后一个开始
while(right>left){
if(nums[i]+nums[left]+nums[right]>0){//值更大 缩小
right=right-1;
}
else if(nums[i]+nums[left]+nums[right]<0){//值更小 扩大
left=left+1;
}
else{
result.push_back(vector<int>{nums[i],nums[left],nums[right]});//储存满足条件的三元组
while(right>left&&nums[right]==nums[right-1]) right--;//右边
while(right>left&&nums[left]==nums[left+1]) left++;//左边
//找到答案 双指针同时收缩
left++;
right--;
}
}
}
return result;
}
};
思路:
①数组排序,然后有一层for循环,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相遇为止。
④a的去重
⑤b、c的去重
例题:力扣18 已完成
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(),nums.end());
//第一层循环
for(int i=0;i<nums.size();i++){
//第一层剪枝,统一通过最后的return返回
if(nums[i]>target&&nums[i]>=0){
break;
}
//第一层去重
if(i>0&&nums[i]==nums[i-1]){
continue;
}
//第二层循环
for(int j=i+1;j<nums.size();j++){
//第二层剪枝
if(nums[i]+nums[j]>target&&nums[i]+nums[j]>=0){
break;
}
//第二层去重
if(j>i+1&&nums[j]==nums[j-1]){
continue;
}
int left=j+1;
int right=nums.size()-1;
while(right>left){
if((long long) nums[i] + nums[j] + nums[left] + nums[right] > target){
right--;
}
else if((long long) nums[i] + nums[j] + nums[left] + nums[right] < target){
left++;
}
else{
result.push_back(vector<int>{nums[i],nums[j],nums[left],nums[right]});
// 对nums[left]和nums[right]去重
while(left<right&&nums[right]==nums[right-1]){
right--; //缩小
}
while(left<right&&nums[left]==nums[left+1]){
left++; //扩大
}
//调整范围
right--;
left++;
}
}
}
}
return result;
}
};
思路:
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,