LintCode有大部分題目來自LeetCode,但LeetCode比較卡,下面以LintCode為平台,簡單介紹我AC的幾個題目,並由此引出一些算法基礎。
1)兩數之和(two-sum)
題目編號:56,鏈接:http://www.lintcode.com/zh-cn/problem/two-sum/
題目描述:
給一個整數數組,找到兩個數使得他們的和等於一個給定的數 target。
你需要實現的函數twoSum需要返回這兩個數的下標, 並且第一個下標小於第二個下標。注意這里下標的范圍是 1 到 n,不是以 0 開頭。
注意:你可以假設只有一組解。
樣例:給出 numbers = [2, 7, 11, 15], target = 9, 返回 [1, 2],即數字2,7
代碼接口:
class Solution {
public:
/*
* @param numbers : An array of Integer
* @param target : target = numbers[index1] + numbers[index2]
* @return : [index1+1, index2+1] (index1 < index2)
*/
vector<int> twoSum(vector<int> &nums, int target) {
// write your code here
}
};
常見的思路是:兩層for循環,任意兩個數組合求其和,判斷是否等於給定的target。但這樣太慢,需要O(n^2)的時間,O(1)的額外空間。可以反過來思考,假如當前選擇了一個數字a,那么為了滿足條件,另一個數字b必須滿足:b=targe-a,即在數組中尋找是否存在b。
如何快速尋找數組中是否存在一個數字b?假如數組是有序的,可以使用二分查找方法,其查找時間復雜度是O(logn)。然而題目並沒給定這個條件。如果對數組排序,首先就要O(nlogn)的時間進行排序,並且排序后,數字的原始下標也要保存,顯然需要O(nlogn)的時間以及O(n)的空間,並不是最好的方法。
如何對一個數組進行快速查找一個元素?算法中提供了一種方法——哈希(Hash),即對數組中的每個元素按照某種方法(hash function)計算其“唯一”值id(稱為哈希值),存儲在新的數組A中(一般稱為哈希數組),並且其下標就是這個“唯一”值。那么如果訪問A[id]存在,則這個元素存在,否則,原始數組中不存在該元素。由於數組是順序存儲的支持隨機訪問,所以查找一個元素是否在數組中,只需要O(1)的時間,但是在初始化哈希數組時,需要O(n)的時間和O(n)的空間。對於某些特定應用中,需要快速的時間,而對空間要求不苛刻時,哈希數組是一個非常好的方法。為了能夠滿足各種應用場景,又衍生出容量大小可以動態增長的哈希集合(hash set)、哈希映射(hash map),STL提供了關於哈希的兩個類:unordered_set和unordered_map,前者只存儲元素,后者可以再增加額外的標志信息。詳細的內容,請自行補充。
由於構造的哈希數組,其元素的下標已經改變了,所以需要額外存儲元素原始的下標,因此此題使用unordered_map<int,int>,其存儲的內容為<元素值,元素原始下標>,詳細代碼:
class Solution {
public:
/*
* @param numbers : An array of Integer
* @param target : target = numbers[index1] + numbers[index2]
* @return : [index1+1, index2+1] (index1 < index2)
*/
/* Tips: find any pair is ok not all pairs.
* using hash map to store the num value and index
* notice: if the target is 4 and the answer expection num 2 + 2,
* only the one num 2 is stored in hash map, but also work ok!
* because must have the other num 2 is not in hash map!
* */
vector<int> twoSum(vector<int> &nums, int target) {
// write your code here
vector<int> v(2,0);
unordered_map<int,int> hash;// val+id
// we can search num and insert it to hash map at same time
// and current num must be not in hash map
for(int i=nums.size(); i--; hash[nums[i]]=i){
if (hash.find(target-nums[i]) == hash.end()) continue;
v[0] = 1 + i; // the index from 1 to n not 0 to n-1
v[1] = 1 + hash[target-nums[i]];
return v;
}
return v; // no answer return {0,0}
}
};
需要注意的是:哈希無法存儲相同元素,因為相同元素有相同的哈希值。如果數組{2,5,6},待求值target=4,沒有解;而數組{2,2,5,6},target=4則有解。如何處理這種情況?可以反向遍歷,初始hash為空,逐漸將已經遍歷過的元素加入到哈希中。
2)三數和(3 sum)
題目編號:57,鏈接:http://www.lintcode.com/zh-cn/problem/3sum/
題目描述:給出一個有n個整數的數組S,在S中找到三個整數a, b, c,找到所有使得a + b + c = 0的三元組。在三元組(a, b, c),要求a <= b <= c。結果不能包含重復的三元組。
樣例:如S = {-1 0 1 2 -1 -4}, 你需要返回的三元組集合的是:(-1, 0, 1),(-1, -1, 2)
這個題目難度增加不少,首先變成3個數的和,然后要求找出所有結果,並且不能重復。但是,返回的只是三元組,並不是原始的下標,如果再使用哈希,那么三個數,需要已知兩個數,即要兩層for循環,那么時間復雜度O(n^2),並且輔助空間也要O(n^2)。有沒有更好地方法?在兩數之和時,曾考慮過排序,然后二分查找。三數和不用返回原始下標,那么用排序+二分查找可否?
首先按升序排序;然后定義下標變量i,j,k,因為是三元組,所以要三個變量。如果簡單的遍歷,那么跟是否有序沒有關系,其時間復雜度將達到O(n^3)。仔細想想:如果當前選擇了a、b、c三個數,如果其和小於目標target,那么需要將其中一個數用更大的數替換;反之亦然。但究竟替換三個數中的哪個數?無法確定就只能先固定兩個變量,讓其第三個變化(替換)。一種辦法是:固定前兩個數i,j,然后讓k在一個范圍中二分變化(二分查找思想),核心代碼如下:
for (int i=0; i<n; ++i){
for (int j=i+1; j<n; ++j){
for (int left=j+1, right=n-1;left!=right;){
int k = (right+left)/2;
int sum = A[i]+A[j]+A[k];
if (sum>target) right = k;
else if (sum<target) left=k;
else {insert(A[i],A[j],A[k]);break;}
}
}
}
拋開一些細節之外,這種方法時間復雜度仍然很大,為O(n^2logn)。仔細觀察發現,k值不是連續變化的,而是兩邊跳躍的。那么可以只固定一個變量i,讓j和k變化。當前值小於target時,可以讓j增加;否則,k減小。完整代碼如下:
class Solution {
public:
/**
* @param numbers : Give an array numbers of n integer
* @return : Find all unique triplets in the array
* which gives the sum of zero.
* each triplet in non-descending order
*/
vector<vector<int> > threeSum(vector<int> &A) {
// write your code here
vector<vector<int> > vs;
int target = 0;
sort(A.begin(),A.end()); // sort A in ascending order
for(int i=0; i<A.size(); ++i){
if (i>0 && A[i-1]==A[i]) continue; // skip duplication
for(int j=i+1, k=A.size()-1; j<k;){
if (j>i+1 && A[j-1]==A[j]){
++j;
continue; // skip duplication
}
if (k<A.size()-1 && A[k]==A[k+1]){
--k;
continue; // skip duplication
}
int sum = A[i]+A[j]+A[k];
if (sum > target) --k;
else if (sum < target) ++j;
else{ // find a triplet
vector v(3,A[i]);
v[1] = A[j++];
v[2] = A[k--];
vs.push_back(v);
}
}
}
return vs;
}
};
注意去除重復的結果。設一個滿足條件的三元組<a,b,c>,如果有重復的三元組與之相同,則說明a,b,c中至少有一個元素的值在數組中出現至少兩次。假如a的值2,在數組中出現多次,則其必然是連續的(數組已經排序),因此可以使用如上的方法去除重復的三元組。該方法時間復雜度O(nlogn)+O(n^2),空間復雜度為O(1)。
3)最接近的三數和(3sum closest)
題目編號:59,題目鏈接:http://www.lintcode.com/zh-cn/problem/3sum-closest/
題目描述:給一個包含 n 個整數的數組 S, 找到和與給定整數 target 最接近的三元組,返回這三個數的和。只需返回最接近的三數和,不需要三個數。
樣例:例如 S = [-1, 2, 1, -4] and target = 1. 和最接近 1 的三元組是 -1 + 2 + 1 = 2.
只需尋找三數和,無需去除重復,顯然,此題比2)簡單得多。可以使用類似的方法,並且實時更新最接近的三數和,這里不再詳述,一種實現代碼:
class Solution {
public:
/**
* @param numbers: Give an array numbers of n integer
* @param target: An integer
* @return: return the sum of the three integers
* the sum closest target.
*/
int threeSumClosest(vector<int> nums, int tar) {
// write your code here
sort(nums.begin(),nums.end());
int ans = INT_MAX;
for(int i=0; i<nums.size(); ++i){
for(int j=i+1, k=nums.size()-1; j<k;){
int sum = nums[i]+nums[j]+nums[k];
// update the closest answer
ans = (abs(tar-sum)<abs(tar-ans) ? sum:ans);
if (sum > tar) --k;
else if (sum < tar) ++j;
else return sum; // sum equal to target
}
}
return ans;
}
};
4)四數和(4 sum)
題目編號:58,題目鏈接:http://www.lintcode.com/zh-cn/problem/4sum/
題目描述:給一個包含n個數的整數數組S,在S中找到所有使得和為給定整數target的四元組(a, b, c, d)。四元組(a, b, c, d)中,需要滿足a <= b <= c <= d,答案中不可以包含重復的四元組。
樣例:例如,對於給定的整數數組S=[1, 0, -1, 0, -2, 2] 和 target=0. 滿足要求的四元組集合為:
(-1, 0, 0, 1),(-2, -1, 1, 2),(-2, 0, 0, 2)
顯然,此題難度大大提高。如果沿用2)的思路,則需要O(n^3)的時間復雜度,但空間為常數級。不妨先試一試:
class Solution {
public:
/**
* @param numbers: Give an array numbersbers of n integer
* @param target: you need to find four elements that's sum of target
* @return: Find all unique quadruplets in the array which gives the sum of
* zero.
*/
/*
* Time O(n^3) , Space O(1)
* */
vector<vector<int> > fourSum(vector<int> A, int tar) {
// write your code here
vector<vector<int> > vs;
sort(A.begin(),A.end()); // ascending order
for(int i=0; i<A.size(); ++i){
if (i>0 && A[i-1]==A[i]) continue; // duplication
for(int j=i+1; j<A.size(); ++j){
if (j>i+1 && A[j-1]==A[j]) continue;// duplication
for(int k=j+1, l=A.size()-1; k<l;){
if (k>j+1 && A[k-1]==A[k]){
++k; // duplication
continue;
}
if (l<A.size()-1 && A[l]==A[l+1]){
--l; // duplication
continue;
}
int sum = A[i]+A[j]+A[k]+A[l];
if (sum > tar) --l;
else if (sum < tar) ++k;
else {
vector<int> v(4,A[i]);
v[1]=A[j], v[2]=A[k++], v[3]=A[l--];
vs.push_back(v);
}
}
}
}
return vs;
}
};
很明顯,需要定義四個下標變量:i,j,k,l,其中固定i,j,讓k和l一個自增,一個自減。同樣需要注意去重復,並且保證每個四元組按升序排列。
如果使用1)的方法,首先將任意兩個元素組合,計算其兩數和並存入哈希;然后再任選兩個數a,b,此時去哈希中尋找是否存在target-a-b。但需要注意的是,具有相同和的二元組,可能不唯一,因此需要一個數組存儲所有和相同的二元組,因此,使用unordered_map<int,vector<pair<int,int> > > twosum;作為哈希映射存儲,其種key表示兩數和的值,數組存儲具有該值的所有二元組,pair<int,int>為具體的二元組的元素值。因此,構建哈希映射的時間、空間復雜度為O(n^2)。
然后再一次定義兩個下標變量i,j,當選擇該i,j時,在哈希映射中可能存在多個二元組的和都為target-a-b,設最多有k個和相同的二元組,則整體時間復雜度為O(k*n^2)。
如何進行去重復?目前沒有很好的辦法。回想一下哈希無法存儲相同的元素,因此再使用一個哈希存儲候選四元組(candidates),對於任意一個滿足體題意的四元組,直接到該哈希中檢驗是否已經存在,從而去重復。下面是一種實現代碼:
class Solution {
public:
// hash functional for hashing the vector
struct hashvec{
size_t operator()(const vector<int> &v)const{
string s;
for (int i=0; i<v.size(); s+=" "+v[i++]);
return hash()(s);
}
};
vector<vector<int> > fourSum(vector<int> A, int tar){
// write your code
sort(A.begin(), A.end()); // ascending order
// using hash map to store the sum of A[i]+A[j] and index i,j
unordered_map<int,vector<pair<int,int> > > twosum;
for (int i=0; i<A.size(); ++i){
if (i>0 && A[i-1]==A[i]) continue; // skip duplication
for (int j=i+1; j<A.size(); ++j) {
if (j>i+1 && A[j-1]==A[j]) continue;// skip duplication
twosum[A[i]+A[j]].push_back(make_pair(i,j));
}
}
unordered_set<vector<int>,hashvec> cans; // test duplication
vector<vector<int> > res; // results
unordered_map<int,vector<pair<int,int> > >::iterator ts;
vector<pair<int,int> >::iterator it;
for (int i=2; i<A.size(); ++i) {
for (int j=i+1; j<A.size(); ++j) {
ts = twosum.find(tar-A[i]-A[j]);
if (ts == twosum.end()) continue; // can't find sum
for (it=ts->second.begin(); it!=ts->second.end(); ++it){
if (it->second >= i) continue; // skip duplication
vector<int> v(4, A[it->first]);
v[1]=A[it->second], v[2]=A[i], v[3]=A[j];
// add the v to both cans and res if cans has no v
if (cans.find(v) == cans.end()){
cans.insert(v);
res.push_back(v);
}
}
}
}
return res;
}
};
其中,struct hashvec是一個哈希仿函數。所謂仿函數,其實質並不是函數,只是表現出來像一個函數一樣。其作用是計算哈希值。因為STL提供的unordered_map只能對基本數據類型進行計算哈希值,哈希值是元素的“唯一”識別碼,之所以帶引號,是因為並不存在一個哈希函數能對任意一個元素計算出唯一的識別碼,當有兩個不同的元素,經過哈希函數計算后,其哈希值相同,那么就需要解決沖突(通常有線性探測和十字鏈表方法,這里不做詳細介紹)。一個好的哈希函數,可以提高哈希的查找速度。對於任意一個四元組,STL並沒有相應的哈希函數進行計算,但是STL對字符串提供了簡單的哈希函數,因此可以利用這一點,將四元組轉化成字符串,從而利用STL自帶的哈希函數hash<string>()進行計算,如上面代碼3~9行。
因此,這種方法雖然額外占用了O(n^2)的空間,但在時間上大大減少,為O(k*n^2),所以對於某些應用還是有參考意義的。
注意:盡管可以使用哈希映射cans進行去重復,但是在生成兩數和的哈希映射twosum時,最好還是進行兩數和的去重復,否則哈希映射太大,也會影響性能。
注:本文涉及的代碼:
two sum:https://git.oschina.net/eudiwffe/lintcode/blob/master/C++/two-sum.cpp
3 sum:https://git.oschina.net/eudiwffe/lintcode/blob/master/C++/3sum.cpp
3 sum closest:https://git.oschina.net/eudiwffe/lintcode/blob/master/C++/3sum-closest.cpp
4 sum:https://git.oschina.net/eudiwffe/lintcode/blob/master/C++/4sum.cpp
