理论基础:
哈希表(Hash Table,也有一些算法书翻译为散列表)
哈希表根据关键码的码直接访问数据的数据结构.
我们经常用的数组就是一张哈希表,哈希表中的关键码就是数组的索引下标,通过下标直接访问数组中的元素

哈希表一般用来快速判断一个元素是否在集合中
例:查询一个名字是否在这所学校的学生名单中
如果使用枚举法,则时间复杂度是O(n),若使用哈希表,则时间复杂度为O(1),我们只需要把这所学校里学生都保存在哈希表里,通过索引就可以查询出这名学生在不在这所学校里了,而这就需要Hash Function(哈希函数)
根本思路:拆分问题,计算一部分并存入,另一部分验证计算
哈希函数把学生的名字直接映射为哈希表上的索引,通过查询索引就可以快速知道这名学生是否在这所学校里
通过hashCode把名字转化为数值,hashCode通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生的名字映射为哈希表上的索引数字了
index=hashFunction(name) hashFunction= hashCode(name)%tableSize
如果hashCode得到的数值大于:哈希表的最大边界(tableSize-1),那么该怎么办呢
此时为了保证映射出来的索引数值都落在哈希表上,我们会再次对数值做一个取模操作,保证学生的名字一定可以映射到哈希表上
此时问题又来了,哈希表就是一个数组,如果学生的数量大于哈希表的大小,那么该怎么办呢?就算哈希函数计算得再均匀,也避免不了会有几名学生的同时映射到哈希表上同一个索引的位置
接下来该哈希碰撞”登场了”
当相同的两个元素映射到一个位置时,这一现象叫做哈希碰撞
哈希碰撞一般有两种解决办法——–拉链法和线性探测法

1.拉链法:
小李和小王在索引1的位置发生了冲突,发生冲突的元素都存储在链表中.这样我们就可以通过索引找到小李和小王了

拉链法就是要选择适当的哈希表大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间
2.线性探测法:
如果使用线性探测法,那么一定要保证tableSize大于dataSize(数据规模).我们需要依靠哈希表中的空位来解决碰撞问题
例如,冲突的位置放了小李的信息,那么就向下找一个空位放置小王的信息.所以要求tableSize一定要大于dataSize,否则哈希表上就没有空位置来存放冲突的数据了

常见的三种哈希结构
使用哈希法解决问题时,一般会选择如下三种数据结构
- 数组
- set(集合)
- map(映射)
在c++中,set的底层实现及优劣如表://需要下标时不宜,只有迭代器
集合 | 底层实现 | 是否有序 | 数组是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
std::set | 红黑树 | 有序 | 否 | 否 | O(logn) | O(logn) |
set::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) |
map(映射)的底层实现及优劣如表://下标优势
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(logn) | O(logn) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) |
std::unordered_map的底层实现为哈希表,std::map和std::multimap的底层实现是红黑树.同理,std::map和std::multimap的key也是有序的(这个问题也经常作为面试题,考查面试者对语言容器底层的理解)
当我们要使用集合来解决哈希问题的时候,优先使用underered_set,因为它的查询和增删效率是最优的,如果要求集合是有序的,那么就使用set,如果要求集合不仅有序,还要有重复数据,那么就使用multiset
map是一个<key,value>的数据结构,map中对key是有限制的(不可修改),对value没有限制,因为key的存储方式是使用红黑树实现的
当我们需要快速判断一个元素是否出现集合中的时候,就要考虑使用哈希法
但哈希法牺牲空间换取了时间,因为我们要使用额外的数组,set或者map来存放数据,才能实现快速的查找
如果在做面试题目的时候遇到需要判断一个元素是否在集合中出现过的场景,则应该第一时间想到哈希法
给定两个字符串 s
和 t
,编写一个函数来判断 t
是否是 s
的字母异位词。
字母异位词 是通过重新排列不同单词或短语的字母而形成的单词或短语,通常只使用所有原始字母一次。
示例 1:
输入: s = "anagram", t = "nagaram" 输出: true
示例 2:
输入: s = "rat", t = "car" 输出: false
提示:
1 <= s.length, t.length <= 5 * 104
s
和t
仅包含小写字母
思路:使用暴力解法的时间复杂度O(n2).下面看一下如何使用哈希法解答这道题目
数组其实就是一个简单的哈希表,而且这道题目中的字符串均为小写字母,这时就可以定义一个数组来记录字符串s中字符出现的次数
需要定义一个多长的数组呢?定义一个叫做record,长度为26的数组就可以了,初始化为0,因为字符a到字符z的ASCII值也是26个连续的数值
首先把字符映射到数组即哈希表的索引上,因为字符a到字符z的ASCII值是26个连续的数值,所以字符a映射为索引0,字符z映射为索引25
遍历字符串s的时候,只需要s[i]-‘a’所在的元素做+1操作即可,并不需要记住字符a的ASCII值,只需求出一个相对数值即可.这样就将字符串s中字符出现的次数统计出来了

如何检查字符串t中是否出现了这些字符呢?在遍历字符串t的时候,对t中出现的字符串映射到哈希表索引上的数值再做减一的操作
如果record数组中的所有元素都为0,则说明字符串s中的字符可以通过改变顺序的方式变成字符串t,返回true
如果record数组中有的元素不为0,则说明不能变换,返回false
代码:
class [[maybe_unused]] Soultion
{
public:
[[maybe_unused]] static bool isAnagram(const string &s, const string &t)
{
int record[26] = {0};
for (char i: s)
{
record[i - 'a']++;
}
for (char i: t)
{
record[i - 'a']--;
}
for (int i: record)
{
if (i != 0)
{
return false;
}
}
return true;
}
};
给定两个数组 nums1
和 nums2
,返回 它们的
交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2] 输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4] 输出:[9,4] 解释:[4,9] 也是可通过的
提示:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
思路:
解答这道题目,主要是学习使用一种哈希数据结构——-unordered_set,它可以解决很多种类类似的问题,存进去就去重了
输出结果中的每个元素一定是唯一的,也就是说,输出的结果是去重的,同时不需要考虑输出结果的顺序
这道题目的暴力解法的时间复杂度是O(n2),下面使用哈希法进一步优化
如果哈希值比较少,特别分散,跨度非常大,那么使用数组就造成了空间的极大浪费
因无序且数值不重复故使用unordered_set

代码:
class [[maybe_unused]] Soultion
{
public:
[[maybe_unused]] static vector<int> intersection(vector<int> &nums1, vector<int> &nums2)
{
unordered_set<int> result_set;
unordered_set<int> nums_set(nums1.begin(), nums1.end());
for (int num: nums2)
{
if (nums_set.find(num) != nums_set.end())//find函数返回的是一个迭代器,如果找到了元素,返回元素的迭代器,如果没有找到,返回的是end()迭代器
{
result_set.insert(num);//存入也去重
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6 输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6 输出:[0,1]
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
- 只会存在一个有效答案
思路:如果使用哈希法,那么本题可以使用map,前面已经讲过了使用数组和set的解题方法,我们分析一下使用数组和set的局限
- 数组的长度是受限制的,如果元素很少而哈希值太大,则会造成内存空间的浪费
- set是一个集合,里面放的元素只能是一个key,而本题不仅要判断y是否存在,而且还要纪录y的下标位置,因为要返回x和y的下标,所以set也不能用
此时就要选择另一种数据结构—–map,它是一种<key,value>的存储结构,可以用key保存数值,用value保存数值所在的下标
本题中无需key有序,且key不允许重复,所以选择std::unordered_map的效率更高
代码:
class [[maybe_unused]] Solution
{
public:
[[maybe_unused]] static vector<int> twoSum(vector<int> &nums, int target)
{
std::unordered_map<int, int> map;//first为存的值,second为下标
for (int i = 0; i < nums.size(); i++)
{
auto iter = map.find(target - nums[i]);//在map中查找是否存在target-nums[i]的值,找的是first,返回其迭代器
if (iter != map.end())//如果存在
{
return {iter->second, i};//只存在一组,返回下标
}
map.insert(pair<int, int>(nums[i], i));//不存在则插入进map,pair使两个数据和为一组插入到insert中
}
return {};//不存在返回空
}
};
给你四个整数数组 nums1
、nums2
、nums3
和 nums4
,数组长度都是 n
,请你计算有多少个元组 (i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2] 输出:2 解释: 两个元组如下: 1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0 2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0] 输出:1
提示:
n == nums1.length
n == nums2.length
n == nums3.length
n == nums4.length
1 <= n <= 200
-228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228
思路:这道题目是四个独立的数组,只要找到A[i]+B[j]+C[k]+D[l]=0就可以了,不用考虑有重复的四种元素相加等于0的情况
解题步骤:
1.定义一个unordered_map,key为a和b两数之和,value为a和b两数之和出现的次数.
2.遍历A,B数组,统计两个数组的元素之和及出现的次数,并放到map中
3.定义int类型的变量count,用来统计a+b+c+d=0出现的次数
4.在遍历C,D数组时,如果0-(c+d)在map中出现,就使用count统计map中key对应的value,即两数之和出现的次数
5.返回统计值count
代码:
class [[maybe_unused]] Solution
{
public:
[[maybe_unused]] static int fourSumCount(vector<int> &A, vector<int> &B, vector<int> &C, vector<int> &D)
{
unordered_map<int, int> umap;//
for (int a: A)
{
for (int b: B)
{
umap[a + b]++;
}
}
int count = 0;
for (int c: C)
{
for (int d: D)
{
if (umap.find(0 - (c + d))->second)//如果存在相同的key,返回的是一个迭代器,迭代器的second是value
{
count += umap[0 - (c + d)];//累计次数
}
}
}
return count;
}
};
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1:
输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。
示例 2:
输入:nums = [0,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。
示例 3:
输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。
提示:
3 <= nums.length <= 3000
-105 <= nums[i] <= 105
哈希解法:(不好写,O3->O2)
通过两层for循环就可以确定a和b的数值了,可以使用哈希法来确定0-(a+b)是否在数组中出现过,这个思路是正确的,但有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组
把符合条件的三元组放进vector中,然后去重,这样是非常耗时的,很容易超时.去重的过程中有很多细节需要注意.时间复杂度可以做到O(n2),但还是比较耗时的,因为不方便做简直操作.
代码:
class [[maybe_unused]] Solution
{
[[maybe_unused]] static vector<vector<int> > threeSum(vector<int> &nums)
{
vector<vector<int> > result;
ranges::sort(nums); //先排序
for (int i = 0; i < nums.size(); i++)
{
if (nums[i] > 0) break; //如果当前数字大于0,则三数之和一定大于0,所以结束循环
if (i > 0 && nums[i] == nums[i - 1]) continue;
unordered_set<int> set;
for (int j = i + 1; j < nums.size(); j++)
{
if (j > i + 2 && nums[j] == nums[j - 1] && nums[j - 1] == nums[j - 2]) continue;//去重操作
if (int c = 0 - nums[i] - nums[j]; set.contains(c))//如果set中存在,则进入
{
result.push_back({nums[i], nums[j], c});//将结果存入result中
set.erase(c);//擦除所有等于c的元素
} else
{
set.insert(nums[j]);//没找到则插入元素
}
}
}
return result;
}
};
双指针法:(好写,O3->O2)
其实这道题目用双指针法更加高效,且易理解
以示例的数组为例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时将一个下标left定义在i+1的位置上,将下标right定义在数组结尾的位置上

还是在数组中找到a,b,c,使得a+b+c=0,这里相当于a=nums[i],b=nums[left],c=nums[right]
接下来如何移动left和right呢?
- 如果nums[i]+nums[left]+nums[irght]>0,则说明此时三数之和大了,因为数组是排序后的数组,所以right下标就应该向左移动,这样才能让三数之和小一些
- 如果nums[i]+nums[left]+nums[irght]<0,则说明此时三数之和小了,此时使left数组向右移动,才能让三数之和大一些
代码:
class Soultion
{
public :
static vector<vector<int> > threeSum(vector<int> &nums)
{
vector<vector<int> > res;
if (nums.size() < 3) //先找排异点,再进入主循环
return res;
ranges::sort(nums);
for (int i = 0; i < nums.size() - 2; i++)
{
if (nums[i] > 0) //先排异
break;
if (i > 0 && nums[i] == nums[i - 1]) //去重,后置去重,去的是后面的,用的是前一个
continue;
int l = i + 1, r = static_cast<int>(nums.size()) - 1;
while (l < r) //没相遇就一直动
{
if (const int sum = nums[i] + nums[l] + nums[r]; sum == 0) //符合
{
res.push_back({nums[i], nums[l], nums[r]}); //压入三元组
while (l < r && nums[l] == nums[l + 1]) //去重逻辑,一直去重到下一个不是重复的
l++;
while (l < r && nums[r] == nums[r - 1]) //去重逻辑,一直去重到下一个不是重复的
r--;
l++;
r--;
} else if (sum < 0)
l++; //left向右
else
r--; //right向左
}
}
return res;
}
};
给你一个由 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
你可以按 任意顺序 返回答案 。
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0 输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8 输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
-109 <= nums[i] <= 109
-109 <= target <= 109
思路:
解答四数之和与三数之和是一个思路,都是使用双指针法,基本解法就是在三数之和解法的基础上再套一层for循环
解法是两层for循环遍历,得到的nums[k]+nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k]+nums[i]+nums[left]+nums[right]==targrt的情况,时间复杂度为O(n3)
同样的,五数之和,六数之和都使用这种解法
双指针法可以降一个次方阶
代码:
class Soultion
{
public:
static vector<vector<int> > fourSum(vector<int> &nums, const int target)
{
vector<vector<int> > res;
ranges::sort(nums);
for (int k = 0; k < nums.size(); k++)
{
if (nums[k] > target) //排异,不知道对不对,但是这样我认为可以减少一些不必要的循环
break;
if (k > 0 && nums[k] == nums[k - 1]) //去重,后置去重,后面的不要
continue;
for (int i = k + 1; i < nums.size(); i++) //i增位
{
if (i > k + 1 && nums[i] == nums[i - 1]) //后置去重,后面的不要
continue;
int l = i + 1, r = static_cast<int>(nums.size()) - 1; //设l,r
while (l < r)
{
if (const int sum = nums[k] + nums[i] + nums[l] + nums[r]; sum < target)
l++;
else if (sum > target)
r--;
else
{
res.push_back({nums[k], nums[i], nums[l], nums[r]});
while (l < r && nums[l] == nums[l + 1])
l++;
while (l < r && nums[r] == nums[r - 1])
r--;
l++, r--;
}
}
}
}
return res;
}
};