有效的字母异位词
力扣链接:题目链接
题目:给你两个字符串,如果这两个字符串的每个字符出现的次数都一样,返回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) |
当我们要使用集合来解决哈希问题的时候,
-
优先使用unordered_set,因为它的查询和增删效率是最优的,、
-
如果需要集合是有序的,那么就用set,
-
如果要求不仅有序还要有重复数据的话,那么就用multiset。
Map同理
回到此题,使用字母连续,可以使用数组解决,让每一个字母映射一个数组一个地址a映射数组索引为0的地址(a-97),以此类推,需要一个容量26的数组,:
cpp
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中出现过,如果出现过就添加到最终集合,最后遍历一遍最终集合以数组形式输出。
cpp
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)(无序哈希表),
cpp
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印象比较深,就可以联想到,数字就是结点,判断是否是循环就是判断是否有环
cpp
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而不是提前判断下一个。
cppwhile (n) { int digit = n % 10; sum += digit * digit; n /= 10; }
两数之和
力扣链接:题目链接
题目:给你一个数组和target,问数组里面的哪两个元素之和等于target,如果等于则返回下标
思路:
遍历数组,把元素值-索引存入map(因为题目最后要求返回索引)
接着第二次遍历数组查找map中是否存在target-元素值的key
cpp
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的存储操作。
cpp
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) 的情况。具体如何实现:
-
计算前两个集群(nums1, nums2)的所有可能的和,以及每个记录和出现的次数,因此使用map。
-
同理计算后两个集群(nums3, nums4)。
-
遍历map1,查找map1中的key是否在map2中存在与之互为相反数的key,若存在,value值相乘即为可能方案,最后将所有方案相加返回。
cpp
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))。
cpp
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时判断字符是否被包含即可(上一个是判断是否相等,因此需要判断哈希桶中是否有多余值存在,需要从头遍历一遍哈希表)
cpp
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与之前相同,答案就会出现重复元组,因此需要在移动指针时跳过相同的元素(同时注意指针不能越界)。
cpp
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个数的位置。
剪枝优化:在进入具体的循环前,先对后续四个数的取值范围进行预判,
因为升序数组,随着指针前移,取值范围会不断变大(右移),所以一旦
当前循环的最小值大于目标值:直接break,已经无解,结束循环;
当前循环的最大值小于目标值:直接continue,当前循环不会有解,进入下一次循环。
cpp
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,进入了死循环,因为外左右指针都没有移动,外左右指针的移动是在内循环中进行的(根据内左右指针最后的移动情况确定移动外左指针还是外右指针),外左右指针的移动还不能里循环的之外进行,那样就又不能动态确定外指针的移动了。
最后最好不要妄想修改完善此方法,不仅费时费脑考虑各种细节,而且剪枝可能还没有一个双指针剪的好