目录
2.1 Leetcode 242.有效的字母异位词(完全一样,只是顺序打乱)
1 理论知识
1. 哈希表是根据关键码的值而直接进行访问的数据结构,即mp[key]访问value。换句话说,哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素。
2. 那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里,时间复杂度O(1)
3. 哈希函数和哈希碰撞(冲突)
哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。
哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。
小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
拉链法:发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了。
常见的哈希结构:数组、集合set、映射map
对比:
4. 简要总结
总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
Day6
2.1 Leetcode 242.有效的字母异位词(完全一样,只是顺序打乱)
代码:
//我的思路:先判断长度是否相等,若相等,先统计每个string里字符出现的次数,再遍历任意一个string,看从头到尾出现的次数是否相同
//时间复杂度O(n),空间复杂度O(n)
bool isAnagram(string s, string t) {
if(s.size()!=t.size())
{
return false;
}
unordered_map<char,int> mp1;
unordered_map<char,int> mp2;
for(int i=0;i<s.length();i++)//s和t长度相等,用size()和length()都行
{
mp1[s[i]]++;
mp2[t[i]]++;
}
for(int i=0;i<s.size();i++)//如果t的长度不等于s的长度,不判断的话,会漏
{
if(mp1[s[i]]!=mp2[s[i]])
{
return false;
}
}
return true;
}
//方法2:分别sort排序,然后再逐一比较(或return s==t;)时间复杂度O(nlgn)
//方法3:代码随想录题解,用数组,时间复杂度O(n),空间复杂度O(1)
2.2 Leetcode349. 两个数组的交集
思路:使用unordered_set,自动去重,题目要求输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序。
//哈希法,使用哈希set,先把一个数组里的数放入哈希set,不重复(或者说自动去重)。再遍历另一个数组,如果找到了就放入容器,同时把这个数去掉。
//此题两个数组谁大谁小都无所谓,因为是求交集
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> st;
vector<int> v_ret;
for(int i=0;i<nums1.size();i++)
//这里的另一种写法,可以直接利用构造函数unordered_set st(nums1.begin(),nums1.end());
{
st.insert(nums1[i]);
}
for(int i=0;i<nums2.size();i++)
{
if(st.find(nums2[i])!=st.end())//找到了,存在
{
v_ret.push_back(nums2[i]);//放入数组
st.erase(nums2[i]);//把这个数从set里去除,防止后面有重复的,再次找到进入数组;如果是用两个set的另一个set,就不用去重了,自动去重
}
}
return v_ret;
}
2.3 Leetcode202 快乐数
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
思路:
题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!
//求某个数每一个位数的平方和
int getSum(int n){
int sum=0;
/*for(int i=n;i>0;i=i/10)//这里一开始后面写成i=n/10了,n不变,导致死循环了,原则是把i看成n
{
int a=i%10;
sum+=a*a;
}*/
//也可用while循环
while(n)//n不等于0时循环
{
int a=n%10;
sum+=pow(a,2);
n=n/10;
}
return sum;
}
//使用哈希法,看sum值到1没或重复过没,重复了则代表死循环了,sum=1时满足条件退出循环
bool isHappy(int n) {
unordered_set<int> st;
int sum=getSum(n);
while(sum!=1)//当sum不为1时循环
{
if(st.find(sum)!=st.end())//一旦满足条件,代表出现过这个sum值,代表死循环了
{
return false;
}
//没有出现过,上面return了,else可加可不加
st.insert(sum);//插入set里
sum=getSum(sum); //更新sum值
}
//退出循环,代表sum==1
return true;
}
2.4 Leetcode1 两数之和
思路:首先我在强调一下 什么时候使用哈希法,当我们需要查询某个元素是否出现过,或者某个元素是否在集合里(如target-nums[i])的时候,就要第一时间想到哈希法。
对于本题,我们不仅要知道元素有没有遍历过,还有知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适。
代码:
//哈希法:用哈希map,key是nums[i]或target-nums[i],value是下标,因为要返回两个下标; set只能找到一个下标
vector<int> twoSum(vector<int>& nums, int target) {
vector<int> v_ret;
unordered_map<int,int> mp;
//用哈希map,key是nums[i],value是下标,因为要返回两个下标
for(int i=0;i<nums.size();i++)
{
//遍历到这一位时,看target-nums[i]出没出现过
if(mp.find(target-nums[i])!=mp.end())
{
v_ret.push_back(i);
v_ret.push_back(mp[target-nums[i]]);
return v_ret; //这3行也可写成return {mp[target-nums[i]],i};不用建vector了
}
//不满足条件,插入哈希表
mp[nums[i]]=i;//用哈希map,key是nums[i],value是下标
}
return v_ret; //写成return{};也行
}
Day7
2.5 Leetcode454 四数相加II
本题是使用哈希法的经典题目。
代码随想录思路:
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 就可以了。
//四个数组两两分组+哈希表,看的题解思路试着写的代码,不要怕用for+for循环;时间复杂度O(n^2)
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> mp;//key是两数之和,后面int是这个和出现的次数
int count=0;//最后返回的数
for(int i=0;i<nums1.size();i++)//遍历前两个数组,把和放到哈希表
{
for(int j=0;j<nums2.size();j++)
{
mp[nums1[i]+nums2[j]]++;
}
}
for(int i=0;i<nums3.size();i++)//遍历后两个数组,看有没有和的相反数
{
for(int j=0;j<nums4.size();j++)
{
if(mp.find(-nums3[i]-nums4[j])!=mp.end())//能找到对应的相反数,说明符合条件
{
count+=mp[-nums3[i]-nums4[j]];//这里不是count加加,因为mp里可能存在多个-(nums3[i]+nums4[j])
}
}
}
return count;
}
2.6 Leetcode383 赎金信(从报纸上剪字)
本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成。
代码:
//方法1:哈希map,看ransomNote是不是magazine的子集
bool canConstruct(string ransomNote, string magazine) {
unordered_map<char,int> mp;//前者代码randsomNote里的字符,后者代表次数
for(int i=0;i<ransomNote.size();i++)
{
mp[ransomNote[i]]++;//获取每个字符的出现次数
}
for(int j=0;j<magazine.size();j++)
{
if(mp.find(magazine[j])!=mp.end())//如果出现过,就把次数减一,最后有没有大于0的,可能小于0,因为magazine更大集合
{
mp[magazine[j]]--;
}
}
for(int i=0;i<ransomNote.size();i++)
{
if(mp[ransomNote[i]]>0)//有大于0的,说明有些字符没找到或找够
{
return false;
}
}
return true;
}
2.7 Leetcode15 三数之和(双指针法)
题干:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
思路:
重点:去重逻辑
1. a的去重
我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
a 如果重复了怎么办,a是nums里遍历的元素,那么应该直接跳过去。
但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。
2. b和c的去重:在找到符合条件的left和right后去重,见代码。
代码:
//哈希表这里不好处理重复,建议使用双指针法,时间复杂度为O(n^2),但三个数的去重也是关键和难点
vector<vector<int>> threeSum(vector<int>& nums) {
//题目给出了length>=3,所以不用判断小于3的情况
vector<vector<int>> v_ret;
//vector<int> v;
sort(nums.begin(),nums.end());//先排序
for(int i=0;i<=nums.size()-3;i++)//i从0遍历到末尾,其实到不了末尾,最多到nums.size()-3,倒数第3个
{
if(nums[i]>0)//已排序,如果最小的数都大于0,那不可能有符合条件的了
{
break;
}
//去重nums[i],如果之前出现过了,那肯定所有符合条件的结果集大收集了,无需再次收集
if(i>0&&nums[i]==nums[i-1])
{
continue;//跳过下面的所有语句,继续for循环
}
int left=i+1;//left在i的右侧一位
int right=nums.size()-1;//right初始一直在末尾
while(left<right)//i不动,移动left和right指针,等到重合时,再移动i
{
if(nums[i]+nums[left]+nums[right]>0)//如果大于0,需要减小,减小right
{
right--;
}
else if(nums[i]+nums[left]+nums[right]<0)//如果小于0,需要增大,移动left
{
left++;
}
else
{
v_ret.push_back(vector<int>{nums[i],nums[left],nums[right]});//注意这种写法
while(left<right&&nums[right]==nums[right-1])//退出循环时,这个nums[right]还等于初始值,只是再向左侧那一位不等了
{
right--;
}
while(left<right&&nums[left]==nums[left+1])//注意等号是==
{
left++;
}
//记住还要移动left和right继续搜索有没有符合条件的
right--;
left++;
}
}
}
return v_ret;
}
2.8 Leetcode18 四数之和
思路:四数之和,和15.三数之和 是一个思路,都是使用双指针法, 基本解法就是在三数之和的基础上再套一层for循环。
四数之和的双指针解法是两层for循环nums[i] + nums[j]为确定值,依然是循环内有left和right下标作为双指针,找出nums[i] + nums[j] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^3) 。
注意去重的点!!
//参考三数之和的思路,相当于三数之和外面又嵌套了一个for循环
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> v_ret;
if(nums.size()<=3)//这个if条件必须判断,加了就AC,(否则[0],0过不去,但[1],0能过去,奇怪!!)
{
return v_ret;
}
sort(nums.begin(),nums.end());//先排序
for(int i=0;i<=nums.size()-4;i++)//下标最多到size-4
{
//cout<<"hh"<<(i<=nums.size()-4)<<endl;//后面竟然为真,第一次循环不判断就进来了!!
//不能判断nums[i]>target返回了,因为可能都是负值,如{-4,-3,-2,-1},target=10
if(nums[i]>target&&nums[i]>=0)//剪枝是为了提高速度,不剪枝也能通过,nums[i]>=0保证越加越大(除了4个0),无论target正负都满足。
{
break;
}
if(i>0&&nums[i]==nums[i-1])//思路是i固定,j/left/right移动,那么i如果和和前一位一样,这种清空已经在前一次i循环已经查过了
{
continue;
}
for(int j=i+1;j<=nums.size()-3;j++)//下标最多到size-3
{
if((nums[i]+nums[j])>target&&(nums[i]+nums[j])>=0)//二级剪枝
{
break;
}
if(j>i+1&&nums[j]==nums[j-1])//类似上面,i不动,j动时去重,
{
continue;
}
int left=j+1; //left指向j后面一位,即i的后面两位
int right=nums.size()-1;//right指向末尾
while(left<right)
{
long long sum=(long long)nums[i]+nums[j]+nums[left]+nums[right];//注意相加之后可能溢出int,long也行,64位linux是8个字节,2^63
if(sum>target)//左侧已最小,或者从最小过来的,不能回去
{
right--;
}
else if(sum<target)
{
left++;
}
else//相等时,要注意去重
{
v_ret.push_back(vector<int>{nums[i],nums[j],nums[left],nums[right]});
while(left<right&&nums[right]==nums[right-1])
{
right--;
}
while(left<right&&nums[left]==nums[left+1])
{
left++;
}
//退出循环时,left和right指向的值还等于初始那个满足条件的值
right--;
left++;
}
}
}
}
return v_ret;
}