哈希(散列表)
哈希也是面试的超高频题,但是一般不需要自己设计哈希函数(常用的要不把输入转换成一个整数然后对素数取模,要不找一个二进制串来做异或),所以哈希的重点跟平衡树很像,只需要知道什么情况下使用hash,不用自己写内部实现,用到的数据结构跟平衡树对应:hash_set和hash_map。面试时可以把hash认为复杂度是O(1)的。
另外,hash经常可以用来代替二分查找,空间换时间,把复杂度降低log(n)。比如:给定一个包含正负数的数组a,找出连续的子数组,使得它的和等于给定的target。这个题跟上面平衡树部分给的例题很像,只是这个是找等于给定target,而不是最接近给定target。这个题的做法是求出sum存到hash中,对于sum[i],我从sum[i+1]~sum[n-1]中寻找是不是存在target+sum[i]这个值,存在的话就可以返回了,不存在i++,然后从hash中删除sum[i+1]这个值。这个做法时空复杂度O(n)+O(n)。不过这个题也暴露了hash跟平衡树相比的缺点:hash只能处理“精确”(相等)的情况,不能处理“模糊”(最接近)的情况。对于平衡树中给出的例子,没法用hash来优化时间复杂度了。
对于哈希的优缺点,做一个总结。
优点:
1. 哈希插入查询快,虽然实际上有collision,但是面试时你可以把它的插入查询认为是O(1)的(这个O(1)的1指的是数据数目,如果插入的是一个string,那它的复杂度还是O(len)的);
2. 哈希跟平衡树在应用上非常像,而平衡树跟有序数组非常像,在查找时发现可以某一个做的话,可以考虑一下其他两个。
缺点:
1. 像上面说的,哈希没法处理模糊的情况。比如2sum(这类问题后面详细总结)问题要求找两个数的和最接近给定target;不过也不要完全觉得哈希没法处理模糊情况,如果2sum还加了个条件,要求这个接近target的程度不能超过某个阈值,比如1,不然也当做不存在的话,那么你可以把元素哈希之后,查找所有符合|a[i]+a[j]-target|<=1,也就是查找三个数是不是存在:target-a[i],target-a[i]-1,target-a[i]+1,就相当于把“模糊”的情况做成精确的了;
2. 哈希使用的空间比较多。这个具体用多少空间还没找到资料,但是以前看过说至少得用两倍的空间保证冲突足够少(当然这个跟你的哈希函数有关);
3. 在实际应用中,哈希也许会成为别人攻击你的方法。方法就是恶意的制造collision,别人怎么攻击你?也许是你暴露了你的哈希函数。Collision多了之后,哈希的这个O(1)查询就名存实亡了;
4. 最后就是哈希函数的设计大有学问。对于基本类型,还有string,STL的hash_map已经有默认的哈希函数了,这个不需要操心,但是如果你要哈希你自己定义的一个structure,问题就出来了,你得自己重新这个哈希函数,这种问题大大滴。
哈希的面试题一般没法单独考(要是说有,就是这么两题:讲讲hash_map的内部实现;实现hash_map的iterator),跟哈希相关的有一类很重要的题,就是关于数组和的。同时这类题也可以给大家体会一下什么时候可以用哈希,很么时候不能用哈希。
这种题我目前能想到的就是这么几个:
1. K-sum,姑且用3-sum来代替,其他类似,从数组中找3个元素和为target;
2. K-sum-closest 姑且也把k看做3,从数组中找出三个数的和最接近target;
3. Sub-vector 找出一个和为target的连续子数组;
4. Sub-vector-closest 找出一个和最近接target的连续子数组;
=================补充一个题,跟哈希没什么关系,但是也是这一类的数组和的题目============================
5. Longest-sub-vector:要求返回一个最长的长度max_len,使得在数组a中存在一个长度max_len的连续子数组,它的和<=给定的target ;
解法是:1.没有负的情况,是双指针贪心,不表。O(n)+O(1)
2.有负的情况是单调队列,可以用二分做到O(nlogn),具体做法是:假设sum[i]是前面i个元素的和,现在要找一个最靠右的和sum[j]使得sum[j]-sum[i]<=target。怎么找sum[j]呢?办法是维护一个min_sum数组,min_sum[i]表示sum[i]到sum[n]之间的最小值。那么min_sum是对于i单调递增的。现在相当于在min_sum中找一个最大的j,使得min_sum[j]-sum[i]<=target。这里就可以用二分查找解决了。
同理,要是找>=target的最长连续子数组,也是可以用这个方法的,只是min_sum变成了max_sum。方法一样
=================补充一个题,跟哈希没什么关系,但是也是这一类的数组和的题目============================
6. max-sum-sub-vector and max-product-sub-vector: 要求返回一个和最大或者积最大的sub-vector。
maxsum的比较好弄,算出sum数组之后,对于当前的sum[i],我只要从[i+1,n-1]中找到一个最大的sum就好了,只需要维护一个数组,max[i]表示[i,n-1]中最大的sum,就可以
O(n)+O(n)时空复杂度解决,有点像第5题的单调队列。
maxproduct以后再补充
===================补充完毕,后面附上代码============================================================
主要是这几种,但不限于这几种(比如还有是找出一个最长的连续子数组和不大于给定target),另外上面每一题都还可以分成有没有负数的情况,所以一共有八种情况。这里总结一下各个题目的解法:
1. 无论有没有负数,都有时空复杂度为O(n^2)+O(1)的解法。思路是排序,然后用三个指针(3sum)i,j,k扫描数组,i指针从0~n-3,对于每个i,j都初始化为i+1,k都初始化为n-1,j和k相向的逼近对方,每次判断sum[i]+sum[j]+sum[k]跟target的大小关系,如果sum==target,那么解出来了;如果sum<target,那么j++(因为对于更小的k,sum肯定只会更小,测试下去已经没有意义了);如果sum>target,同理,k--。这样就可以保证检测过了所有可能的解。
2. 这个题方法跟1类似,不同的地方是,现在无论sum跟target的大小关系如果,都要用一个if语句判断一下,到底现在sum跟target是不是足够接近,用来更新结果。
这两个题的解都没有用到哈希= =!是不是离题了~是有点~~~原因是这样的,第一题还有一个O(n^2)的解法(不过空间上也要O(n)),不需要排序,对于每一组i,j,我们都从剩下的元素里面找是不是存在target-a[i]-a[j]这个值,而剩下的元素存到了一个hash_set里面,所以,可以用O(1)的时间找到,同时随着i,j的改变,我们要维护这个hash_set。但是,这个方法却不能用到题目2上面,因为它不是找的一个精确解。这两题在leetcode上面都有,在这篇博客最后会附上链接和code。
3,4这两个题都是对正负很敏感的,换句话说就是数组有负数跟没有负数,是完全不一样的题。先讲没有负数的情况:
3. 贪心性质,时空复杂度O(n)+O(1)。用两个指针st和end同时从前往后扫描,用一个sum来记录[st,end)这个子数组的和,当sum==target时,找到解;当sum>target是,st++;当sum<target时,end++。这样就可以保证遍历了所有可能的情况。
4. 4跟3的关系就跟1跟2的关系一样,这里就不写了,应该可以想到。
可以看出,在没有负数的时候,3跟4的解法是跟1,2雷同的。顺带一提,这种要求连续子数组和的题目,经常都可以用一个sum数组很方便的处理,前面这两题也可以,只不过这种贪心算法会更加有效。sum数组的做法就是先用一个新的数组sum来记录前面的元素和,sum[i] = a[0]+a[1]+......+a[i],那么子数组[i,j]的和就是sum[j]-sum[i-1],这样的话任意连续指数组都可以表示成sum数组的两个元素差,接下来怎么搞就具体题目具体分析了。下面是讨论3,4有负数的情况:
3. 显然,有负数时,这种贪心性质就用不上了,因为当前的sum>0不意味着继续加下去不会出现等于target的时候。那是不是就不能做到O(n)了呢?其实还是可以的,利用前面讲到的sum数组的方法,加上本篇博客的重点:哈希,就可以做到O(n),当然这时候就需要额外空间了。具体做法是:先求出sum数组,然后把所有sum数组的元素放到一个hash_map里面,然后遍历sum数组,每遍历一个,就先从hash_map里面把这个值删了(只删一个,所以hash_map可以定义成hash_map<int,int>,后一个int指有多少个一样的key),然后查找是不是存在sum[i]+target这个值,存在的话,就成功了,否则继续找下去。另外,这个sum数组是不用真的存起来的,所以,额外空间都是被hash_map用了。怎么不用存sum数组你应该可以想到的。
4. 这题就更麻烦了,由于变成了“模糊”的情况,哈希发挥不了作用了。有了前面sum数组的思路,最暴力的方法:对每个sum[i],线性去查找后面的跟sum[i]的差最接近target的元素,o(n^2)+O(n)。不过,根据前面哈希跟平衡树的关系,你应该已经想到可以在3的基础上,用平衡树代替哈希了。这次用的是map<int,int>,map提供了lower_bound,upper_bound的方法,分别用这两个方法去找最接近sum[i]+target的元素。算法复杂度O(nlogn)+O(n)。
总结成一个表格吧:
没有负数 | 有负数 | |
3-sum |
排序,三指针遍历
o(n^2)+O(1)
| 同左 |
3-sum-closest |
排序,三指针遍历
o(n^2)+O(1)
| 同左 |
sub-vector |
贪心,双指针遍历
O(n)+O(1)
|
求sum数组,哈希查询
O(n)+O(n)
|
sub-vector-closest |
贪心,双指针遍历
O(n)+O(1)
|
求sun数组,平衡树查询
O(nlogn)+O(n)
|
Longest-sub-vector |
贪心,双指针遍历
O(n)+O(1)
|
求min_sum数组,单调队列
O(nlogn)+O(n)
|
关于3sum,3sum-closest的题目,leetcode上有原题:
class Solution {
public:
vector<vector<int> > threeSum(vector<int> &num) {
sort(num.begin(),num.end());
vector<vector<int>> result;
for(int i=0;i<(int)num.size()-2;i++){
int j = i+1, k=(int)num.size()-1;
while(j<k){
int sum = num[i]+num[j]+num[k];
if(sum==0){
result.push_back(vector<int>());
result.back().push_back(num[i]);
result.back().push_back(num[j]);
result.back().push_back(num[k]);
}
if(sum<=0){
j++;
while(j<k&&num[j-1]==num[j])
j++;
}else{
k--;
while(k>j&&num[k+1]==num[k])
k--;
}
}
while(i<(int)num.size()-2&&num[i+1]==num[i])
i++;
}
return result;
}
};
千万注意这个sum.size(),它的返回值是一个unsigned int,如果你直接用它减去一个整数,它会变成一个很大的整数。。。一开始被这个坑了不少,所以,要不你一开始把它赋给一个int,比如int size = num.size(); 要不你强制类型转换。
class Solution {
public:
vector<vector<int> > fourSum(vector<int> &num,int target) {
sort(num.begin(),num.end());
vector<vector<int>> result;
for(int i=0;i<(int)num.size()-3;i++){
for(int t=i+1;t<(int)num.size()-2;t++){
int j = t+1, k=(int)num.size()-1;
while(j<k){
int sum = num[i]+num[j]+num[k]+num[t];
if(sum==target){
result.push_back(vector<int>());
result.back().push_back(num[i]);
result.back().push_back(num[t]);
result.back().push_back(num[j]);
result.back().push_back(num[k]);
}
if(sum<=target){
j++;
while(j<k&&num[j-1]==num[j])
j++;
}else{
k--;
while(k>j&&num[k+1]==num[k])
k--;
}
}
while(t<(int)num.size()-2&&num[t+1]==num[t])
t++;
}
while(i<(int)num.size()-3&&num[i+1]==num[i])
i++;
}
return result;
}
};
code:
class Solution {
public:
int threeSumClosest(vector<int> &num, int target) {
sort(num.begin(),num.end());
int closest = num[0]+num[1]+num[2];
for(int i=0;i<num.size()-2;i++){
int j=i+1,k=num.size()-1;
while(j<k){
int sum = num[i]+num[j]+num[k];
if(abs(closest-target)>abs(sum-target))
closest = sum;
if(sum>=target)
k--;
else j++;
}
}
return closest;
}
};
上面补充的题目,没有OJ,code:
int MaxLen(vector<int>& a,int target){
if(a.empty()) return 0;
vector<int> min_sum(a.size());
int sum = 0,max_len=0;
min_sum[0] = a[0];
for(int i=1;i<a.size();i++)
min_sum[i] = min_sum[i-1]+a[i];
for(int i=(int)a.size()-2;i>=0;i--)
min_sum[i] = min(min_sum[i+1],min_sum[i]);
for(int i=0;i<a.size();i++){
int l=i,r=a.size()-1;
while(l<=r){
int mid = l+(r-l)/2;
if(target+sum>=min_sum[mid])
l = mid+1;
else r = mid-1;
}
sum += a[i];
max_len = max(max_len,r-i+1);
}
return max_len;
}