面试总结之-哈希算法分析

哈希(散列表)

  哈希也是面试的超高频题,但是一般不需要自己设计哈希函数(常用的要不把输入转换成一个整数然后对素数取模,要不找一个二进制串来做异或),所以哈希的重点跟平衡树很像,只需要知道什么情况下使用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上有原题:

3Sum

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(); 要不你强制类型转换。

4Sum

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;
    }
};



3Sum Closest

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;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章