閱讀735 返回首頁    go 阿裏雲 go 技術社區[雲棲]


LeetCode之K sum problem

做過leetcode的人都知道, 裏麵有2sum, 3sum(closest), 4sum等問題, 這些也是麵試裏麵經典的問題, 考察是否能夠合理利用排序這個性質, 一步一步得到高效的算法. 經過總結, 本人覺得這些問題都可以使用一個通用的K sum求和問題加以概括消化, 這裏我們先直接給出K Sum的問題描述和算法(遞歸解法), 然後將這個一般性的方法套用到具體的K, 比如leetcode中的2Sum, 3Sum, 4Sum問題. 同時我們也給出另一種哈希算法的討論.

leetcode求和問題描述(K sum problem):

K sum的求和問題一般是這樣子描述的:給你一組N個數字(比如 vector<int> num), 然後給你一個常數(比如 int target) ,我們的goal是在這一堆數裏麵找到K個數字,使得這K個數字的和等於target。

注意事項(constraints):

注意這一組數字可能有重複項:比如 1 1 2 3 , 求3sum, 然後 target  = 6, 你搜的時候可能會得到 兩組1 2 3, 1 2 3,1 來自第一個1或者第二個1, 但是結果其實隻有一組,所以最後結果要去重。

K Sum求解方法, 適用leetcode 2Sum, 3Sum, 4Sum:

方法一: 暴力,就是枚舉所有的K-subset, 那麼這樣的複雜度就是 從N選出K個,複雜度是O(N^K)

方法二: 排序,這個算法可以考慮最簡單的case, 2sum,這是個經典問題,方法就是先排序,然後利用頭尾指針找到兩個數使得他們的和等於target, 這個2sum算法網上一搜就有,這裏不贅述了,給出2sum的核心代碼:

01 //2 sum
02 int i = starting; //頭指針
03 int j = num.size() - 1; //尾指針
04 while(i < j) {
05     int sum = num[i] + num[j];
06     if(sum == target) {
07         store num[i] and num[j] somewhere;
08         if(we need only one such pair of numbers)
09             break;
10         otherwise
11             do ++i, --j;
12     }
13     else if(sum < target)
14         ++i;
15     else
16         --j;
17 }

2sum的算法複雜度是O(N log N) 因為排序用了N log N以及頭尾指針的搜索是線性的,所以總體是O(N log N),好了現在考慮3sum, 有了2sum其實3sum就不難了,這樣想:先取出一個數,那麼我隻要在剩下的數字裏麵找到兩個數字使得他們的和等於(target – 那個取出的數)就可以了吧。所以3sum就退化成了2sum, 取出一個數字,這樣的數字有N個,所以3sum的算法複雜度就是O(N^2 ), 注意這裏複雜度是N平方,因為你排序隻需要排一次,後麵的工作都是取出一個數字,然後找剩下的兩個數字,找兩個數字是2sum用頭尾指針線性掃,這裏很容易錯誤的將複雜度算成O(N^2 log N),這個是不對的。我們繼續的話4sum也就可以退化成3sum問題,那麼以此類推,K-sum一步一步退化,最後也就是解決一個2sum的問題,K sum的複雜度是O(n^(K-1))。 這個界好像是最好的界了,也就是K-sum問題最好也就能做到O(n^(K-1))複雜度,之前有看到過有人說可以嚴格數學證明,這裏就不深入研究了。

K Sum (2Sum, 3Sum, 4Sum) 算法優化(Optimization):

這裏講兩點,第一,注意比如3sum的時候,先整體排一次序,然後枚舉第三個數字的時候不需要重複, 比如排好序以後的數字是 a b c d e f, 那麼第一次枚舉a, 在剩下的b c d e f中進行2 sum, 完了以後第二次枚舉b, 隻需要在 c d e f中進行2sum好了,而不是在a c d e f中進行2sum, 這個大家可以自己體會一下,想通了還是挺有幫助的。第二,K Sum可以寫一個遞歸程序很優雅的解決,具體大家可以自己試一試。寫遞歸的時候注意不要重複排序就行了。

K Sum (2Sum, 3Sum, 4Sum) 算法之3sum源代碼(不使用std::set)和相關開放問題討論:

因為已經收到好幾個網友的郵件需要3sum的源代碼, 那麼還是貼一下吧, 下麵的代碼是可以通過leetcode OJ的代碼(又重新寫了一遍, 於Jan, 11, 2014 Accepted), 就當是K sum的完整的一個case study吧, 順便解釋一下上麵的排序這個注意點, 同時我也有關於結果去重的問題可以和大家討論一下, 也請大家集思廣益, 發表意見, 首先看源代碼如下:

01 class Solution {
02 public:
03     vector<vector<int> > threeSum(vector<int> &num) {
04         vector<vector<int> > vecResult;
05         if(num.size() < 3)
06             return vecResult;
07  
08         vector<int> vecTriple(3, 0);
09         sort(num.begin(), num.end());
10         int iCurrentValue = num[0];
11         int iCount = num.size() - 2; // (1) trick 1
12         for(int i = 0; i < iCount; ++i) {
13             if(i && num[i] == iCurrentValue) { // (2) trick 2: trying to avoid repeating triples
14                 continue;
15             }
16             // do 2 sum
17             vecTriple[0] = num[i];
18             int j = i + 1;
19             int k = num.size() - 1;
20             while(j < k) {
21                 int iSum = num[j] + num[k];
22                 if(iSum + vecTriple[0] == 0) {
23                     vecTriple[1] = num[j];
24                     vecTriple[2] = num[k];
25                     vecResult.push_back(vecTriple); // copy constructor
26                     ++j;
27                     --k;
28                 }
29                 else if(iSum + vecTriple[0] < 0)
30                     ++j;
31                 else
32                     --k;
33             }
34             iCurrentValue = num[i];
35         }
36                 // trick 3: indeed remove all repeated triplets
37                 // trick 4: already sorted, no need to sort the triplets at all, think about why?
38         vector< vector<int> >::iterator it = unique(vecResult.begin(), vecResult.end());
39         vecResult.resize( distance(vecResult.begin(), it) );
40         return vecResult;
41     }
42 };

首先呢, 在K Sum問題中都有個結果去重的問題, 前文也說了, 如果輸入中就有重複元素的話, 最後結果都需要去重, 去重有好幾個辦法, 可以利用std::set的性質(如leetcode上3sum的文章, 但是他那個文章的問題是, set沒用好, 導致最終複雜度其實是O(N^2 * log N), 而非真正的O(N^2) ), 可以利用排序(如本文的方法)等, 去重本身不難, 難的是不利用任何排序或者std::set直接得到沒有重複的triplet結果集. 本人試圖通過已經排好序這個性質來做到這一點(試圖不用trick 3和4下麵的兩條語句), 但是經過驗證這樣的代碼(沒有trick 3, 4下麵的兩行代碼, 直接return vecResult)也不能保證結果沒有重複,於是不得不加上了trick 3, 4,還是需要通過在結果集上進一步去重. 筆者對於這個問題一直沒有很好的想法, 希望這裏的代碼能拋磚引玉, 大家也討論一下有沒有辦法, 或者利用排序的性質或者利用其它方法, 直接得到沒有重複元素的triplet結果集, 不需要去重這個步驟.

那麼還是解釋一下源代碼裏麵有四個trick, 以及筆者試圖不利用任何std::set或者排序而做到去重的想法. 第一個無關緊要順帶的小trick 1, 是說我們排好序以後, 隻需要檢測到倒數第三個數字就行了, 因為剩下的隻有一種triplet 由最後三個數字組成. 接下來三個trick都是和排序以及最後結果的去重問題有關的, 我一起說.

筆者為了達到不需要在最後的結果集做額外的去重, 嚐試了以下努力: 首先對輸入數組整體排序, 然後使用之前提到的3sum的算法, 每次按照順序先定下triplet的第一個數字, 然後在數組後麵尋找2sum, 由於已經排好序, 為了防止重複, 我們要保證triplet的第一個數字沒有重複, 舉個例子, -3, – 3, 2, 1, 那麼第二個-3不應該再被選為我們的第一個數字了, 因為在第一個-3定下來尋找2 sum的時候, 我們一定已經找到了所有以-3為第一個數字的triplet(trick 2).  但是這樣做盡管可以避免一部分的重複, 但是還有另一種重複無法避免: -3, -3, -3, 6, 那麼在定下第一個-3的時候, 我們已經有兩組重複triplet <-3, -3, 6>, 如何在不使用std::set的情況下避免這類重複, 筆者至今沒有很好的想法. 大家有和建議? 望不吝賜教!

Hash解法(Other):

其實比如2sum還是有線性解法的,就是用hashmap, 這樣你check某個值存在不存在就是常數時間,那麼給定一個sum, 隻要線性掃描, 對每一個number判斷sum – num存在不存在就可以了。注意這個算法對有重複元素的序列也是適用的。比如 2 3 3 4 那麼hashtable可以使 hash(2) = 1; hash(3) = 1, hash(4) =1其他都是0,  那麼check的時候,掃到兩次3都是check sum – 3在不在hashtable中,注意最後返回所有符合的pair的時候也還是要去重。這樣子推廣的話 3sum 其實也有O(N^2)的類似hash算法,這點和之前是沒有提高的,但是4sum就會有更快的一個算法。

4sum的hash算法:

O(N^2)把所有pair存入hash表,並且每個hash值下麵可以跟一個list做成map, map[hashvalue] = list,每個list中的元素就是一個pair, 這個pair的和就是這個hash值,那麼接下來求4sum就變成了在所有的pair value中求 2sum,這個就成了線性算法了,注意這裏的線性又是針對pair數量(N^2)的線性,所以整體上這個算法是O(N^2),而且因為我們掛了list, 所以隻要符合4sum的我們都可以找到對應的是哪四個數字。

到這裏為止有人提出這個算法不對 (感謝Jun提出這點!! See the comment below), 因為這裏的算法似乎無法檢查取出來的四個數字是否有重複的, 也就是說在轉換成2sum問題得到的那些個pair中, 有可能會有重複元素, 比如說原來數組中的第一個元素其實是重複了兩次才使得4 sum滿足要求, 那麼這樣得到的四元組(四個數字的和等於給定的值), 其實隻有三個原數組元素, 其中第一個元素用了兩次, 那麼這樣就不對了. 如果僅從我上麵的描述來看, 確實是沒有辦法檢查重複的, 但是仔細想想我們隻要在map中存pair的的時候記錄下的不是原數組對應的值, 而是原數組的id,就可以避免這個問題了. 更加具體的, map[hashvalue] = list, 每個list的元素就是一個pair, 這個pair<int, int> 中的pair是原來的array id, 使得這兩個id對應到元素組中的元素值的和就是這個hash值. 那麼問題就沒有了, 我們在轉換成的2sum尋找所有pair value的2sum的同時要檢查最後得到的四元組<id1, id2, id3, id4>沒有重複id. 這樣問題就解決了.

結束語:

這篇文章主要想從一般的K sum問題的角度總結那些比較經典的求和問題比如leetcode裏麵的2sum, 3sum(closest), 4sum等問題,  文章先直接給出K Sum的問題描述和算法(遞歸解法), 然後將這個一般性的方法套用到具體的K, 比如leetcode中的2Sum, 3Sum, 4Sum問題. 同時我們也給出另一種哈希算法的討論. 那麼這篇文章基本上還是自己想到什麼寫什麼,有疏忽不對的地方請大家指正,也歡迎留言討論,如果需要源代碼,請留言或者發郵件到info@tech-wonderland.net

最後更新:2017-04-03 12:54:36

  上一篇:go Tomcat中兩個不同項目共享Session
  下一篇:go linux驅動開發--字符設備:自旋鎖