【LeetCode】4.哈希表与部分双指针系列
总目录:
0.理论基础
0.1.要点
哈希表,散列表,HashTable,一种key-value集合,最重要的是其查找速度为O(1);
哈希函数,建立key和哈希地址的映射关系,哈希函数有不同的实现方式;
哈希冲突/碰撞是指不同的key产生的哈希地址重复,有不同的策略处理这种问题,但不在此考虑;
常见的哈希结构:数组、set集合、map键值对集合;
0.2.比较
set和multiset的底层实现是红黑树(平衡搜索二叉树),有序,multiset允许重复
unordered_set的底层实现为哈希表,无序
map和multimap的底层实现是红黑树,有序,multimap允许重复
unordered_map的底层实现是哈希表,无序
使用选择:
当需要使用集合来解决查询问题时,优先考虑unordered,
如果需要集合是有序的就用基础的set/map,如果还要求允许重复则使用multi,
所谓哈希法:
虽然set\multiset\map\multimap使用红黑树来存储数据,但也使用哈希函数来做映射,对于这种使用数据结构来解决
映射问题的方法统称为哈希法。
0.3.总结
当需要快速判断一个元素是否在集合中时,考虑哈希法。
哈希法是牺牲了空间换时间,因为要开辟额外的空间实现某种有序才能实现快速的查找。
1.有效的字母异位词
1.1.问题描述
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。
链接:https://leetcode.cn/problems/valid-anagram
1.2.要点
查询出现的次数,要统计、要查询
1哈希表,map
2固定长度26的数组来统计即可
1.3.代码实例
哈希表map

1 #include <map> 2 class Solution { 3 public: 4 bool isAnagram(string s, string t) { 5 if(s.length()!=t.length()){ 6 return false; 7 } 8 9 int dataLen=s.length(); 10 map<char,int> charMap; 11 for(int i=0;i<s.length();i++){ 12 charMap[s[i]]++; 13 charMap[t[i]]--; 14 } 15 16 for(int i=0;i<26;i++){ 17 if(charMap['a'+i]!=0){ 18 return false; 19 } 20 } 21 22 return true; 23 } 24 };
2.查找共用字符串
2.1.问题描述
给你一个字符串数组 words ,请你找出所有在 words 的每个字符串中都出现的共用字符( 包括重复字符),并以数组形式返回。你可以按 任意顺序 返回答案。
输入:words = ["bella","label","roller"] 输出:["e","l","l"]
链接:https://leetcode.cn/problems/find-common-characters
2.2.要点
数组,对于数据空间已知的集合,可以使用数组来统计
哈希
2.3.代码实例
数组

1 class Solution { 2 public: 3 vector<string> commonChars(vector<string>& A) { 4 vector<string> result; 5 if (A.size() == 0) return result; 6 int hash[26] = {0}; // 用来统计所有字符串里字符出现的最小频率 7 for (int i = 0; i < A[0].size(); i++) { // 用第一个字符串给hash初始化 8 hash[A[0][i] - 'a']++; 9 } 10 11 int hashOtherStr[26] = {0}; // 统计除第一个字符串外字符的出现频率 12 for (int i = 1; i < A.size(); i++) { 13 memset(hashOtherStr, 0, 26 * sizeof(int)); 14 for (int j = 0; j < A[i].size(); j++) { 15 hashOtherStr[A[i][j] - 'a']++; 16 } 17 // 更新hash,保证hash里统计26个字符在所有字符串里出现的最小次数 18 for (int k = 0; k < 26; k++) { 19 hash[k] = min(hash[k], hashOtherStr[k]); 20 } 21 } 22 // 将hash统计的字符次数,转成输出形式 23 for (int i = 0; i < 26; i++) { 24 while (hash[i] != 0) { // 注意这里是while,多个重复的字符 25 string s(1, i + 'a'); // char -> string 26 result.push_back(s); 27 hash[i]--; 28 } 29 } 30 31 return result; 32 } 33 };
哈希

1 #include <map> 2 using namespace std; 3 class Solution { 4 public: 5 vector<string> commonChars(vector<string>& words) { 6 int dataLen=words.size(); 7 vector<string> ret; 8 if(dataLen<1){ 9 return ret; 10 } 11 12 int strLen=0; 13 map<char,int> baseMap,curMap; 14 //取基准值 15 strLen=words[0].length(); 16 for(int j=0;j<strLen;j++){ 17 baseMap[words[0][j]]++; 18 } 19 20 21 for(int i=1;i<dataLen;i++){ 22 curMap.clear(); 23 strLen=words[i].length(); 24 25 //统计当前字符串中的频率 26 for(int j=0;j<strLen;j++){ 27 curMap[words[i][j]]++; 28 } 29 30 //取交集 31 for(int k=0;k<26;k++){ 32 baseMap['a'+k]=min(baseMap['a'+k],curMap['a'+k]); 33 } 34 } 35 36 for(auto iter = baseMap.rbegin();iter!=baseMap.rend();iter++){ 37 if(iter->second<1){ 38 continue; 39 } 40 for(int i=0;i<iter->second;i++){ 41 string str(1,iter->first); 42 ret.push_back(str); 43 } 44 } 45 46 return ret; 47 } 48 };
3.两个集合的交集
3.1.问题描述
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
链接:https://leetcode.cn/problems/intersection-of-two-arrays/
3.2.要点
哈希,用nums1填充哈希表,遍历nums2来查哈希表
3.3.代码实例
哈希,注意auto&和emplace_back的细节

1 class Solution { 2 public: 3 vector<int> intersection(vector<int>& nums1, vector<int>& nums2) { 4 vector<int> ans; 5 unordered_set<int> us{ nums1.begin(), nums1.end() }; 6 for (auto& iter : nums2) { 7 if (us.find(iter) != us.end()) { 8 us.erase(iter); 9 ans.emplace_back(iter); 10 } 11 } 12 return ans; 13 } 14 };
4.快乐数
4.1.问题描述
今天你快乐吗~
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
(1)对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
(2)然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
(3)如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
例子1:输入:n = 19 输出:true 解释:
例子2:输入:n = 2 输出:false
链接:https://leetcode.cn/problems/happy-number
4.2.要点
可以证明n不会发散增长
1哈希
观察例子1和2即可知所谓不是快乐数意味着该数字在迭代计算过程中会再次出现,导致计算陷入死循环。
用哈希统计每次计算出的数字是否出现过即可。
2快慢指针
对于一个数字计算其每位数平方和的操作可以看做->next,这样就会形成一个链表,接下来的问题就是这个链表是否存在环。
slow指针每次走1步,fast每次走2步,如果fast能等于slow则存在环,如果fast到达1则不存在环、n是快乐数
4.3.代码实例
哈希

1 class Solution { 2 public: 3 int getSquareSum(int val){ 4 if(val==0){ 5 return 0; 6 } 7 int ret=0; 8 while(val!=0){ 9 ret+=pow(val%10,2); 10 val/=10; 11 } 12 return ret; 13 } 14 15 bool isHappy(int n) { 16 if(n==0){ 17 return false; 18 } 19 20 unordered_set<int> nums; 21 while(n!=1){ 22 //该值重复出现,陷入循环,不是快乐数 23 if(nums.find(n) != nums.end()){ 24 return false; 25 } 26 27 nums.insert(n); 28 29 n=getSquareSum(n); 30 } 31 32 return true; 33 } 34 };
快慢指针

1 class Solution { 2 public: 3 int bitSquareSum(int n) { 4 int sum = 0; 5 while(n > 0) 6 { 7 int bit = n % 10; 8 sum += bit * bit; 9 n = n / 10; 10 } 11 return sum; 12 } 13 14 bool isHappy(int n) { 15 int slow = n, fast = n; 16 do{ 17 if(n==1){ 18 return true; 19 } 20 21 slow = bitSquareSum(slow); 22 fast = bitSquareSum(fast); 23 fast = bitSquareSum(fast); 24 }while(slow != fast); 25 26 return slow == 1; 27 } 28 };
5.两数之和
5.1.问题描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
链接:https://leetcode.cn/problems/two-sum
5.2.要点
1哈希
取A,计算Sum-A是否在集合中,注意A=Sum/2的情况
5.3.代码实例
哈希

1 class Solution { 2 public: 3 vector<int> twoSum(vector<int>& nums, int target) { 4 unordered_map<int, int> hashtable; 5 for (int i = 0; i < nums.size(); ++i) { 6 auto it = hashtable.find(target - nums[i]); 7 if (it != hashtable.end()) { 8 return {it->second, i}; 9 } 10 hashtable[nums[i]] = i; 11 } 12 return {}; 13 } 14 };
6.四数之和之4个独立集合
6.1.问题描述
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
(1)0 <= i, j, k, l < n
(2)nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
例子:
输入: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
链接:https://leetcode.cn/problems/4sum-ii
6.2.要点
1哈希
还是采用已知x,哈希查找Sum-y是否存在的方法
采用分为两组,HashMap 存一组,另一组和 HashMap 进行比对。
这样的话情况就可以分为三种:
(1)HashMap 存一个数组,如 A。然后计算三个数组之和,如 BCD。时间复杂度为:O(n)+O(n^3),得到 O(n^3).
(2)HashMap 存三个数组之和,如 ABC。然后计算一个数组,如 D。时间复杂度为:O(n^3)+O(n),得到 O(n^3).
(3)HashMap存两个数组之和,如AB。然后计算两个数组之和,如 CD。时间复杂度为:O(n^2)+O(n^2),得到 O(n^2).
根据上述我们可以得出要存两个数组算两个数组。
我们以存 AB 两数组之和为例。首先求出 A 和 B 任意两数之和 sumAB,以 sumAB 为 key,sumAB 出现的次数为 value,存入 hashmap 中。
然后计算 C 和 D 中任意两数之和的相反数 sumCD,在 hashmap 中查找是否存在 key 为 sumCD。
算法时间复杂度为 O(n2)。
6.3.代码实例
哈希

1 class Solution { 2 public: 3 int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) { 4 int dataLen=nums1.size(); 5 int ret=0; 6 int tempSum=0; 7 8 //统计A+B 9 unordered_map<int,int> numsAB; 10 for(int i=0;i<dataLen;i++){ 11 for(int j=0;j<dataLen;j++){ 12 tempSum=nums1[i]+nums2[j]; 13 numsAB[tempSum]++; 14 } 15 } 16 17 //对比CD 18 for(int i=0;i<dataLen;i++){ 19 for(int j=0;j<dataLen;j++){ 20 tempSum = 0 - nums3[i] - nums4[j]; 21 if(numsAB.find(tempSum) != numsAB.end()){ 22 ret += numsAB[tempSum]; 23 } 24 } 25 } 26 27 return ret; 28 } 29 };
7.赎金信
7.1.问题描述
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
链接:https://leetcode.cn/problems/ransom-note
7.2.要点
1哈希,使用额度消耗问题
7.3.代码实例
哈希

1 class Solution { 2 public: 3 bool canConstruct(string ransomNote, string magazine) { 4 if (ransomNote.size() > magazine.size()) { 5 return false; 6 } 7 vector<int> cnt(26); 8 for (auto & c : magazine) { 9 cnt[c - 'a']++; 10 } 11 for (auto & c : ransomNote) { 12 cnt[c - 'a']--; 13 if (cnt[c - 'a'] < 0) { 14 return false; 15 } 16 } 17 return true; 18 } 19 };
8.三数之和
8.1.问题描述
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
链接:https://leetcode.cn/problems/3sum
8.2.要点
1.排序+双指针
(1)特判,对于数组长度 n,如果数组为 null或者数组长度小于 3,返回 []。
(2)对数组进行排序。
(3)遍历排序后数组:i为遍历索引
若 nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 0,直接返回。
对于重复元素:跳过,避免出现重复解
进入双指针搜索,令左指针 L=i+1,右指针 R=n−1,当 L<R 时,执行循环:
当 nums[i]+nums[L]+nums[R]==0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。并同时将 L,R移到下一位置,寻找新的解
若和大于 0,说明 nums[R]太大,R左移
若和小于 0,说明 nums[L] 太小,L右移
8.3.代码实例
排序+双指针

1 class Solution { 2 public: 3 vector<vector<int>> threeSum(vector<int>& nums) { 4 int n = nums.size(); 5 sort(nums.begin(), nums.end()); 6 vector<vector<int>> ans; 7 8 int second = 0, third = 0; 9 int curSum=0; 10 // 枚举 first 11 for (int first = 0; first < n; first++) { 12 //最小值已大于0 13 if(nums[first]>0){ 14 break; 15 } 16 // 需要和上一次枚举的数不相同 17 if (first > 0 && nums[first] == nums[first - 1]) { 18 continue; 19 } 20 21 //开始双指针搜索 22 second= first + 1; 23 third = n - 1; 24 while(second < third){ 25 curSum = nums[first] + nums[second] + nums[third]; 26 if(curSum == 0){ 27 //左右边界去重 28 while((second+1)<=third && nums[second]==nums[second+1]){ 29 second++; 30 } 31 while((third-1)>=second && nums[third]==nums[third-1]){ 32 third--; 33 } 34 35 //记录组合 36 ans.push_back({nums[first], nums[second], nums[third]}); 37 38 //同时移动左右指针 39 second++; 40 third--; 41 } 42 if(curSum<0){ 43 second++; 44 } 45 if(curSum>0){ 46 third--; 47 } 48 } 49 } 50 return ans; 51 } 52 };
9.四数之和
9.1.问题描述
给你一个由 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
你可以按 任意顺序 返回答案 。
链接:https://leetcode.cn/problems/4sum
9.2.要点
类比三数之和
如果暴力解法n层循环嵌套,那么时间复杂度是O(n^4),
参考三数之和的排序之后双指针的方法,双指针可以去掉一层幂运算,这里套用排序+双指针后的时间复杂度为O(n^3+nlogn)=O(n^3)
9.3.代码实例
排序+双指针

1 class Solution{ 2 public: 3 vector<vector<int>> fourSum(vector<int>& nums, int target) { 4 sort(nums.begin(),nums.end()); 5 vector<vector<int> > res; 6 if(nums.size()<4) 7 return res; 8 int a,b,c,d,_size=nums.size(); 9 for(a=0;a<=_size-4;a++){ 10 if(a>0&&nums[a]==nums[a-1]) continue; //确保nums[a] 改变了 11 //剪枝 12 if ((long) nums[a] + nums[a + 1] + nums[a + 2] + nums[a + 3] > target) { 13 break; 14 } 15 if ((long) nums[a] + nums[_size - 3] + nums[_size - 2] + nums[_size - 1] < target) { 16 continue; 17 } 18 19 for(b=a+1;b<=_size-3;b++){ 20 if(b>a+1&&nums[b]==nums[b-1])continue; //确保nums[b] 改变了 21 //剪枝 22 if ((long) nums[a] + nums[b] + nums[b + 1] + nums[b + 2] > target) { 23 break; 24 } 25 if ((long) nums[a] + nums[b] + nums[_size - 2] + nums[_size - 1] < target) { 26 continue; 27 } 28 29 c=b+1,d=_size-1; 30 while(c<d){ 31 32 //注意防止溢出 33 if(nums[a]+nums[b]-target<-(nums[c]+nums[d])) 34 c++; 35 else if(nums[a]+nums[b]-target>-(nums[c]+nums[d]))//同上 36 d--; 37 else{ 38 res.push_back({nums[a],nums[b],nums[c],nums[d]}); 39 while(c<d&&nums[c+1]==nums[c]) //确保nums[c] 改变了 40 c++; 41 while(c<d&&nums[d-1]==nums[d]) //确保nums[d] 改变了 42 d--; 43 c++; 44 d--; 45 } 46 } 47 } 48 } 49 return res; 50 } 51 };
10.总结
10.1.双指针要点
双指针的主要用途
(1)链表中特定的计算行经路程的问题;
(2)降低时间复杂度,用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为O(n)
xxx.问题
xxx.1.问题描述
111
xxx.2.要点
222
xxx.3.代码实例
333
本文作者:OhOfCourse
本文链接:https://www.cnblogs.com/OhOfCourse/p/16978626.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步