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