当前位置: 首页 > news >正文

跟着Carl学算法--哈希表

有效的字母异位词

力扣链接:题目链接

题目:给你两个字符串,如果这两个字符串的每个字符出现的次数都一样,返回true

思路:当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set (集合)
  • map(映射)
集合底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
std:set红黑树有序O(log n)O(log n)
std:multiset红黑树有序O(logn)O(logn)
std:unordered_ set哈希表无序O(1)O(1)
映射底层实现是否有序数值是否可以重复能否更改数值查询效率增删效率
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不可修改0(1)0(1)

当我们要使用集合来解决哈希问题的时候,

  1. 优先使用unordered_set,因为它的查询和增删效率是最优的,、

  2. 如果需要集合是有序的,那么就用set,

  3. 如果要求不仅有序还要有重复数据的话,那么就用multiset。

Map同理

回到此题,使用字母连续,可以使用数组解决,让每一个字母映射一个数组一个地址a映射数组索引为0的地址(a-97),以此类推,需要一个容量26的数组,:

class Solution {
public:bool isAnagram(string s, string t) {int hash[26]={0};        for(char i:s)hash[i-97]+=1;for(char i:t)hash[i-97]-=1;for(int i:hash){if(i!=0){return false;}}return true;}
};

两个数组的交集

力扣链接:题目链接

题目:求两个数组的交集

思路:因为数字大小随机,就不能使用数组了(映射不方便),(后来测试数据改为了1000以内了,可以创建1000的数组,空间换时间了)。

因为两个数组中都有可能有重复元素,因此需要两个set或者map(map,其实没必要,有些浪费vaule空间了),
一个set1存储数组1的元素,然后遍历数组2的元素是否在集合set1中出现过,如果出现过就添加到最终集合,最后遍历一遍最终集合以数组形式输出。

class Solution {
public:vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {unordered_set<int> set1;unordered_set<int> result;vector<int> ans;for (int i : nums1) {set1.emplace(i);}for (int i : nums2) {if (set1.contains(i)) {result.emplace(i);}}for(int i:result){ans.emplace_back(i);}return ans;}
};

快乐数

力扣链接:题目链接

题目:求解一个数是不是快乐数

『快乐数]定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为1,也可能是无限循环但始终变不到1.如果这个过程结果为1,那么这个数就是快乐数。
  • 如果 (n 是快乐数就返回true ;不是,则返回 false

思路:关键是确定循环条件是什么,如何让循环结束

如果是快乐数,那得到1就可以结束了,那如果不是快乐数,那循环结束的条件应该是什么?当然是接下来的循环是无效循环,再循环下去已经没有意义了。就像题目中给出的提示“无限循环”只要当前的数之前已经得到过了,那就没有可以结束循环了

哈希表,将已经计算过的值存入哈希表,查询更快,效率为O(1)(无序哈希表),

class Solution {
public:int getNext(int n) {int sum = 0;while (n) {int digit = n % 10;sum += digit * digit;n /= 10;}return sum;}bool isHappy(int n) {unordered_set<int> set;// 当新添加的数与集合内的数字相同时结束循环while (n != 1&&!set.count(n)) {// 不为1的新数存放到集合set.insert(n);//求下一个数n = getNext(n);// 为1时结束循环}return n==1;}
};

双指针:如果对上一阶段链表的题目的环形链表II印象比较深,就可以联想到,数字就是结点,判断是否是循环就是判断是否有环

class Solution {
public:int getNext(int n) {int sum = 0;while (n) {int digit = n % 10;sum += digit * digit;n /= 10;}return sum;}bool isHappy(int n) {int slow = n;int fast = getNext(n); //快指针先走,避免刚开始就符合结束条件while (fast != 1 && slow != fast) {slow = getNext(slow);          // 慢指针走一步fast = getNext(getNext(fast)); // 快指针走两步}return fast == 1;}
};

虽然两者的时间复杂度相同,都为O(log n),但是哈希表插入时,内部会哈希值计算、可能的哈希冲突处理、可能的内存重新分配、存储中间值的内存消耗等等,一系列内在消耗,还是建议使用双指针。

循环、环形等就使用双指针

注意:

  • 封装函数后,系统会进行优化,优化为内联函数,不会多出过多的消耗的,能封装为函数就尽可能封装。

  • 能手搓就尽量不调用

  • 求平方和的标准写法,直接判断当前数是否为0而不是提前判断下一个

while (n) {int digit = n % 10;sum += digit * digit;n /= 10;
}

两数之和

力扣链接:题目链接

题目:给你一个数组和target,问数组里面的哪两个元素之和等于target,如果等于则返回下标

思路:

遍历数组,把元素值-索引存入map(因为题目最后要求返回索引)
接着第二次遍历数组查找map中是否存在target-元素值的key

class Solution {
public:vector<int> twoSum(vector<int>& nums, int target) {unordered_map<int, int> map;for (int i = 0; i < nums.size(); i++) {map.emplace(nums[i], i);}for (int i = 0; i < nums.size(); i++) {if (map.count(target - nums[i]) && i != map[target - nums[i]]) //由于map是独立存在数组的副本,需要额外判断map中的元素和数组中匹配的元素是否是同一个(题目要求不能使用两次)return {i, map[target - nums[i]]};}return {};}
};

遍历数组,把元素值-索引移入map,并查找map中是否存在target-元素值的key(是以上方法的优化),少了额外的相同元素判断,以及提前终止会少几次map的存储操作。

class Solution {
public:vector<int> twoSum(vector<int>& nums, int target) {unordered_map<int, int> map;for (int i = 0; i < nums.size(); i++) {if (map.count(target - nums[i])) {return { map[target - nums[i]], i };}map[nums[i]] = i;}return {};}
};

四数相加II

力扣链接:题目链接

题目:给你4个数组,每个数组n个元素,各取每个数组的1个元素,相加等0,一共几种方案

思路:没错,就是基于上一道两数之和的,将4数之和转化为2组2数之和本质上是找 A + B = -(C + D) 的情况。具体如何实现:

  1. 计算前两个集群(nums1, nums2)的所有可能的和,以及每个记录和出现的次数,因此使用map。

  2. 同理计算后两个集群(nums3, nums4)。

  3. 遍历map1,查找map1中的key是否在map2中存在与之互为相反数的key,若存在,value值相乘即为可能方案,最后将所有方案相加返回。

class Solution {
public:int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {if(nums1.size()==0){return 0;}unordered_map<int,int>map1;unordered_map<int,int>map2;for(int i:nums1){for(int j:nums2){map1[i+j]+=1;}}for(int i:nums3){for(int j:nums4){map2[i+j]+=1;}}int sum=0;for(auto pair:map1){if(map2.contains(-pair.first)){sum+=pair.second*map2[-pair.first];}}return sum;}
};

优化:

可以将2、3步合并,直接在遍历3、4数组时就查找合适的结果。
省去3、4数组所有可能在map2的存储空间,以及map1的遍历(虽然是效率是O(1))。

class Solution {
public:int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {if(nums1.size()==0){return 0;}unordered_map<int,int>map1;for(int i:nums1){for(int j:nums2){map1[i+j]+=1;}}int sum=0;for(int i:nums3){for(int j:nums4){if(map1.contains(-i-j)){sum+=map1[-i-j];}}}return sum;}
};

赎金信

力扣链接:题目链接

题目:给你字符串A和B,问你A能不能由B里面的字符组成,B的字符每个只能用一次

思路:是有效的字母异位词的简单版,甚至不需要最后遍历一次哈希表,只要在遍历A时判断字符是否被包含即可(上一个是判断是否相等,因此需要判断哈希桶中是否有多余值存在,需要从头遍历一遍哈希表)

class Solution {
public:bool canConstruct(string ransomNote, string magazine) {int hash[26] = {0};for (char i : magazine) {hash[i - 97] += 1;}for (char i : ransomNote) {hash[i - 97] -= 1;if (hash[i - 97] < 0) {return false;}}return true;}
};

虽然简单,第一个一次OC有思路的

三数之和

力扣链接:题目链接

题目:给你一个整数数组,判断数组中是否存在三个数和为0,返回所有的所有和为 0 且不重复的三元组。

思路:两层for循环就可以确定 a 和b 的数值了,可以使用哈希法来确定 0-(a+b) 是否在数组里出现过,然后使用索引实现元素不复用。
但是要实现元组不重复很难,比如{1,1,-1,-1,0},需要对a、b、c都进行重复判断

这种时候使用双指针更好,

首先将数组排序,然后有一层for循环,遍历数组,即a,在开始前需要对a进行去重,如果a的值与前一次相同就需要跳过。

双指针分别分别从剩余数组的两端向中间移动,根据三数之和的情况移动左右指针,等于0后取值,类似于二分法

取值后需要对左右指针继续收缩,但是如果下一个b、c与之前相同,答案就会出现重复元组,因此需要在移动指针时跳过相同的元素(同时注意指针不能越界)。

class Solution {
public:vector<vector<int>> threeSum(vector<int>& nums) {sort(nums.begin(), nums.end());vector<vector<int>> result;for (int i = 0; i < nums.size(); i++) {if (nums[i] > 0) //最小值大于0已经不会有满足要求的解了,直接返回return result;if (i > 0 && nums[i - 1] == nums[i]) //a去重continue;int left = i + 1;int right = nums.size() - 1;while (left < right) {if (nums[i] + nums[left] + nums[right] > 0) {right--;continue;}if (nums[i] + nums[left] + nums[right] < 0) {left++;continue;}result.push_back({nums[i], nums[left], nums[right]});while (left<right&&nums[left] == nums[++left]); //b去重while (left<right&&nums[right] == nums[--right]); //c去重}}return result;}
};

四数之和

力扣链接:题目链接

题目:给你一个数组和一个target,从数组里面取4个元素进行相加,等于target的结果有多少个。四元组不能重复。

思路:是三数之和的进阶版,只要在三数之和最外层再加一层指针即可,要注意去重和剪枝细节即可。a、b、c、d分别代表第1、2、3、4个数的位置。

剪枝优化:在进入具体的循环前,先对后续四个数的取值范围进行预判,image-20241102123233653

因为升序数组,随着指针前移,取值范围会不断变大(右移),所以一旦

当前循环的最小值大于目标值:直接break,已经无解,结束循环;

当前循环的最大值小于目标值:直接continue,当前循环不会有解,进入下一次循环。

class Solution {
public:vector<vector<int>> fourSum(vector<int>& nums, int target) {vector<vector<int>> results;int n = nums.size();// 如果数组元素不足4个,直接返回if (n < 4)return results;// 排序sort(nums.begin(), nums.end());// 第一层固定for (int a = 0; a < n - 3; a++) {// 去重if (a > 0 && nums[a] == nums[a - 1])continue;// 优化:如果最小可能和大于target,则后续无解if ((long long)nums[a] + nums[a + 1] + nums[a + 2] + nums[a + 3] >target)break;// 优化:如果最大可能和小于target,继续下一个if ((long long)nums[a] + nums[n - 3] + nums[n - 2] + nums[n - 1] <target)continue;// 第二层固定for (int b = a + 1; b < n - 2; b++) {// 去重if (b > a + 1 && nums[b] == nums[b - 1])continue;// 优化:如果最小可能和大于target,则后续无解if ((long long)nums[a] + nums[b] + nums[b + 1] + nums[b + 2] >target)break;// 优化:如果最大可能和小于target,继续下一个if ((long long)nums[a] + nums[b] + nums[n - 2] + nums[n - 1] <target)continue;// 双指针int c = b + 1;int d = n - 1;while (c < d) {long long current_sum =(long long)nums[a] + nums[b] + nums[c] + nums[d];if (current_sum < target) {c++;} else if (current_sum > target) {d--;}else {results.push_back({nums[a], nums[b], nums[c], nums[d]});// 去重 (无论是否相等都移到下一位)while (c < d && nums[c] == nums[++c]);while (c < d && nums[d] == nums[--d]);}}}}return results;}
};

本来还想使用两组双指针解决的,里外各一组,因为最后要遍历数组,结束条件一定是里左右指针相遇了,根据里指针的最后移动情况,比如最后是里左指针移动的,说明数太小了,就移动外左指针,进行下一次循环。但是这样就不能进行剪枝了,因为一旦在里循环同层之前continue,进入了死循环,因为外左右指针都没有移动,外左右指针的移动是在内循环中进行的(根据内左右指针最后的移动情况确定移动外左指针还是外右指针),外左右指针的移动还不能里循环的之外进行,那样就又不能动态确定外指针的移动了。

最后最好不要妄想修改完善此方法,不仅费时费脑考虑各种细节,而且剪枝可能还没有一个双指针剪的好

相关文章:

  • 千博政府网站管理系统企业网站建设平台
  • 网站建设的类型或分类百度识图在线识别
  • 定制开发电商网站建设多少钱楚雄百度推广电话
  • 域名注册完成后如何做网站seo排名关键词点击
  • 武汉市江夏区建设局网站站长统计app最新版本2023
  • 未来前景比较好的行业有哪些进一步优化
  • Kafka如何保证消息可靠?
  • 构建你的 AI 模块宇宙:Spring AI MCP Server 深度定制指南
  • 哈希表理论与算法总结
  • TCP/UDP协议深度解析(一):UDP特性与TCP确认应答以及重传机制
  • Leaking GAN
  • Netty内存池核心PoolArena源码解析
  • 搭建智能问答系统,有哪些解决方案,比如使用Dify,LangChain4j+RAG等
  • 《C++初阶之类和对象》【初始化列表 + 自定义类型转换 + static成员】
  • Python光学玻璃库opticalglass
  • IP证书在网络安全中的作用
  • Windows驱动开发最新教程笔记2025(一)名词解释
  • Label Studio安装和使用
  • ABP VNext + BFF(Backend for Frontend)模式:Angular/React 专用聚合层
  • 总结设置缓存的时机
  • 七天学会SpringCloud分布式微服务——01
  • 基于C#实现(WinForm)P2P聊天小程序
  • 操作系统---内存管理之虚拟内存
  • React性能优化:父组件如何导致子组件重新渲染及避免策略
  • 【JavaScript-Day 48】告别 Ajax,拥抱现代网络请求:Fetch API 完全指南
  • HarmonyOS开发基础 --面向鸿蒙的TypeScript基础语法一文入门