代码随想录刷题记录 - 哈希表
记录了一些比较经典的题目,难度适中,新手向!
期待更好的思路和解法及指正!
目录
一、简单题
242. 有效的字母异位词
- 题目描述:
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
- 示例:
- 思路:
由于字母都是小写,且在ASCII表上小写字母的排列是有序连续的,因此可以想到哈希表的方式,将字母对应a的相对位置映射到序号为0-25的数组上。两种方法都实现这样的映射,不过在处理上有所不同。
- 哈希计数法1:
简单粗暴分别得到两个字符串对应的哈希表,再逐元素判断是否相等即可;
时间复杂度:O(n)
空间复杂度:O(n)
- 哈希计数法2:
在解法一的基础上进行简化,若只使用一个哈希表,那么得到字符串s的哈希表后,若字符串t满足是s的字母异位词,那么对哈希表的对应位置进行自减操作,得到的哈希表最终应该为所有元素为0。一来省下了空间,二来简化了算法的步骤和一些双哈希表会出现的额外判断。
时间复杂度:O(n)
空间复杂度:O(n)
- 代码:
class Solution {
public:
bool isAnagram(string s, string t) {
int len1 = s.size();
int len2 = t.size();
if(len1!=len2) return false;
//初始化
int *arr1 = new int[26];
for(int i =0;i<26;i++) arr1[i] = 0;
int *arr2 = new int[26];
for(int i =0;i<26;i++) arr2[i] = 0;
//各字符串统计字母个数
for(int i =0;i<len1;i++) arr1[ s[i]-97 ]++;
for(int i =0;i<len2;i++) arr2[ t[i]-97 ]++;
int i; bool NoCharIsSame = true;
for(i = 0;i<26;i++) //排除毫无关系字符串
{
if(arr1[i]>0 && arr2[i]>0)
{
NoCharIsSame = false;
break;
}
}
if(NoCharIsSame) return false;
else{
//判断是否每个字母个数相等
for(i = 0;i<26;i++)
{
if(arr1[i]!=arr2[i]) return false;
}
if(i==26) return true; //无异常则迭代到最后下标
return false;
}
}
};
class Solution {
public:
bool isAnagram(string s, string t) {
int len1 = s.size();
int len2 = t.size();
if(len1!=len2) return false;
int *arr = new int[26];
for(int i =0;i<26;i++) arr[i] = 0;
//各字符串统计字母个数
for(int i =0;i<len1;i++) arr[ s[i]-97 ]++;
for(int i =0;i<len2;i++) arr[ t[i]-97 ]--;
for(int i =0;i<26;i++)
{
if(arr[i]!=0) return false;
}
return true;
}
};
349. 两个数组的交集
- 题目描述:
给定两个数组 nums1 和 nums2 ,返回它们的交集 。输出结果中的每个元素一定是唯一 的。我们可以不考虑输出结果的顺序 。
- 示例:
解法一:纯数组法
思路:
- 判断是否有相交元素:对nums1进行遍历,就某一元素在nums2中search,若返回值为-1则表明不是相交元素,对其进行懒惰删除(假删除,赋值为-1),进行第一轮排序;
- 去重:由于nums1已排序,因此从非-1的元素开始,前后判断若重复则对前一个懒惰删除,进行第二轮排序;
- 整理:将nums1非-1的元素部分所对应的子数组赋值到新的数组上,返回。
代码:
class Solution {
int search(vector<int>& arr,int key)
{
int len = arr.size();
int res = -1;
for(int i = 0;i<len;i++){
if(arr[i] == key)
{
res = i;
break;
}
}
return res;
}
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size();
int x;
for(int i =0;i<len1;i++)
{
x = search(nums2,nums1[i]);
if(x == -1){ //nums[i]不是相交元素
nums1[i] = -1; //懒惰删除
}
}
//对nums1去重
sort(nums1.begin(),nums1.end()); //排序
int fast = 0;
while(nums1[fast]==-1) fast++;
for(fast = fast+1;fast<len1;fast++)
{
if(nums1[fast-1] == nums1[fast]){
nums1[fast-1] = -1;
}
}
//整理
sort(nums1.begin(),nums1.end()); //排序
fast = 0;
while(nums1[fast]==-1) fast++;
int newLen = len1-fast; //新数组长度
vector<int> resArr(newLen); //创建答案数组
int offset = fast; //偏移量
for(;fast<len1;fast++)
{
resArr[fast-offset] = nums1[fast];
}
return resArr;
}
};
解法二:set结构的哈希法
此解法是针对关键字的数值范围不确定(可能非常大)的情况下比较适用的。
思路:
- 将
nums1
利用std::unordered_set的构造函数转换为set
,完成去重的目的; - 对
nums2
进行遍历,对每一个元素在set
中find,若找到则为相交元素,存入结果集合; - 将结果集合通过vector的构造函数转换为
vector
并返回;
代码:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
std::unordered_set<int> result_set; //结果集合,去重
std::unordered_set<int> nums_set(nums1.begin(),nums1.end());
for(int num :nums2){ //取交集
if( nums_set.find(num) != nums_set.end() )
{
result_set.insert(num); //相交元素插入结果集合
}
}
return vector<int>( result_set.begin(),result_set.end() ); //集合转化为vector
}
};
解法三:数组结构的哈希法
由于力扣后面改了题目要求,使得关键字的值大小有了限制,因此采用数组结构的哈希表也可以实现。
思路:
由于哈希表用数组来实现,因此set只用来完成去重的操作,最后再将其转化为vector即可。
代码:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
std::unordered_set<int> result_set;
//创建哈希表
int hashTable[1001] = {0};
//记录nums1情况
for(int i : nums1) hashTable[i] = 1;
//求交集
for(int i : nums2)
if(hashTable[i] == 1) result_set.insert(i);
//返回答案
return vector<int>(result_set.begin(),result_set.end());
}
};
1.两数之和
噩梦开始的地方。
- 题目描述:
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
提示:
2 <= nums.length <= 104
-109 <= nums[i] <= 109
-109 <= target <= 109
只会存在一个有效答案
- 示例:
解法一:有序化数组法
思路:
该方法采用纯数组来实现,首先是一个拷贝数组arr,将其排序后用双指针的方法左右开弓,挤出合适的两个元素(因为一定会有答案,所以不会有死循环)。
接着就这两个元素在原数组中找寻对应的下标再返回即可。
由于拷贝数组后的元素是有序的,回到原数组查找会出现很多不可避免的条件判断,显得代码较冗余。
分析:
时间复杂度:O(n)
空间复杂度:O(n)
代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> arr(nums);
sort(arr.begin(),arr.end()); //排序
int fast = arr.size()-1; int slow = 0;
vector<int> ans(2);
//有序排列下找到对应的两个值(两边向中挤)
while(arr[slow] + arr[fast] != target && fast>slow)
{
if(arr[slow] + arr[fast] < target) slow++;
else if(arr[slow] + arr[fast] > target) fast--;
}
bool repeated = false; //重复标记
//在原数组里找到两个元素的下标
for(int i =0;i<nums.size();i++)
{
if(nums[i] == arr[slow])
{
if(repeated)
{
ans[1] = i;
break;
}
ans[0] = i;
repeated = true;
}
else if(nums[i] == arr[fast]) ans[1] = i;
}
return ans;
}
};
解法二:哈希map
思路:
剖析:在上一个方法的基础上,会发现有很多地方是可以优化的:例如存储结构的选择上——有没有既可以存放值又存放下标的结构呢(除了自定义struct外);找到互补元素的算法显得很复杂;由于拷贝了数组再排序后还得回到原数组找下标,不仅多了很多时间还省不了很多奇怪的条件判断.....
因此新的解法选择了unordered_map
作为存储遍历过的元素的结构。其底层实现是采用哈希映射,因此在查找和插入元素上耗费的时间均是O(1),同时基于该结构所制定的算法也大大轻便有效。
代码:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
std::unordered_map<int,int> hashTable;
for(int i = 0;i<nums.size();i++)
{
std::unordered_map<int,int> ::iterator iter = hashTable.find(target-nums[i]);
if(iter != hashTable.end()) //找到了互补元素
{
return {iter->second,i};
}
hashTable.insert( pair<int,int>(nums[i],i) );
}
return {};
}
};
- 代码的语法用到了迭代器,还不是很熟,后面回来填坑.......
二、中等题
454. 四数相加 II
- 题目描述:
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
- 示例:
- 思路:
除去暴力枚举,要想到怎么把O(n^4)的时间复杂度降成O(n^2),那么就不能够是4层循环,可以采用两两捆绑的方式。优化思路是:
- 在
nums1和nums2
的整体中,将key1+key2
的每一种可能放入查找表中,由于可能有多种key1+key1
的组合,值相同,因此需要用哈希表来存储。存储时还要考虑用什么样的存储结构,由于还需要将对应key的出现次数做记录,因此需要用到std::unordered_map
的<key,value>键值对来实现存储; - 接下来,在
nums3和nums4
的整体中,对挨个求和的值取负后在哈希表中寻找是否存在记录,若存在则count+=记录对应的value; - 最后返回count。
大佬分析:
- 分析:
- 代码:
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> hashTable;
for(int key1 : nums1)
{
for(int key2 :nums2)
{
hashTable[key1 + key2]++;
}
}
int count = 0;
for(int key3 : nums3)
{
for(int key4 : nums4)
{
//找map中的相反数
if( hashTable.find( -(key3 + key4) ) != hashTable.end() )
count += hashTable[ -(key3 + key4) ];
}
}
return count;
}
};
15. 三数之和
- 题目描述:
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
注意:答案中不可以包含重复的三元组。
- 示例:
- 分析题目:
题目不同于前面的四数相加或者三数相加是在不同的数组里找到符合条件的元素组个数,而是在同一个数组中找寻满足条件的元素组,同时还需要对找到的结果进行去重。
如何将这个O(n^3)的算法复杂度降下来有如下两种方法:
解法一:双指针法
思路:
对数组进行一趟循环遍历,在每一轮中采用定一动二的方式找寻满足条件的三元组。在这个算法中着重注意去重的操作。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector< vector<int> > result;
//排序
sort(nums.begin(), nums.end());
int left,right;
for(int i = 0; i<nums.size(); i++)
{
if( nums[i] > 0 ) break;
//对a去重
if( i >0 && nums[i] == nums[i-1] ) continue;
left = i+1;
right = nums.size() - 1;
while(left < right)
{
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] }); //加入结果集
//对c去重
while(left<right && nums[right] == nums[right-1] ) right--;
//对b去重
while(left<right && nums[left] == nums[left+1] ) left++;
right--; left++;
}
}
}
return result;
}
};
解法二:哈希法
好复杂的哈希法.....
18. 四数之和
- 题目描述:
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
- 示例:
- 思路:
四数之和是三数之和的提升版,思路也可以在其之上进行改进。只需要在三数之和的基础上再增加多一层循环,采用定一定一动二的方式,同时注意好剪枝操作和去重操作即可。
- 分析:
- 时间复杂度:
O(n^3)
四数之和的双指针解法是两层for循环nums[k] + nums[i]
为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target
的情况,三数之和的时间复杂度是O(n^2)
,四数之和的时间复杂度是O(n^3)
。
- 空间复杂度:O(n)
- 代码:
class Solution {
public:
//-2 -1 0 0 1 2
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector< vector<int> > result;
if(nums.size() <4 ) return {};
//排序
sort(nums.begin(), nums.end());
int left,right;
for(int j = 0; j <nums.size(); j++)
{
//剪枝一
if(nums[j] > target && (nums[j] >=0 || target >= 0)) break;
//对a去重
if(j>0 && nums[j] == nums[j-1]) continue;
for(int i = j+1; i<nums.size(); i++)
{
//剪枝二
if( nums[j]+nums[i] >target && (target>=0|| nums[j]+nums[i]>=0) ) break;
//对b去重
if( i >j+1 && nums[i] == nums[i-1] ) continue;
left = i+1;
right = nums.size() - 1;
while(left < right)
{
if( (long)nums[j]+nums[i]-target > -(nums[left]+nums[right]) ) right--;
else if( (long)nums[j]+nums[i]-target<-(nums[left]+nums[right])) left++;
else{
result.push_back( vector<int>{ nums[j],nums[i],nums[left],nums[right] }); //加入结果集
//对d去重
while(left<right && nums[right] == nums[right-1] ) right--;
//对c去重
while(left<right && nums[left] == nums[left+1] ) left++;
right--; left++;
}
}
}
}
return result;
}
};
总结
- Q1:为什么会想到用哈希表:
答:要根据题目的需求来,如果是想找某一个元素是否存在(类似于n数求和,先求出n-1个之和sum,再在哈希表中找target-sum)、某一元素是否遍历过等即想要快速找到某一个元素,那么哈希表会很好的解决这一问题。
能将查找的复杂度将O(n)降至O(1)。
- Q2:哈希表为什么用map\set\array
set:
map:
使用时机:
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。
- map是用来存什么的?\set?\array?
答:map存储键值对key-value,set存储key且能去重,array存储key但空间有限。
一刷end:2023.1.24
致谢&部分资源出处:代码随想录