面試總結之-哈希算法分析

哈希(散列表)

  哈希也是面試的超高頻題,但是一般不需要自己設計哈希函數(常用的要不把輸入轉換成一個整數然後對素數取模,要不找一個二進制串來做異或),所以哈希的重點跟平衡樹很像,只需要知道什麼情況下使用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;
}


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