LeetCode 刷題指南(一):為什麼要刷題

Candy_GL發表於2018-08-14

轉自:https://www.jianshu.com/p/7bfbaf893a34

雖然刷題一直飽受詬病,不過不可否認刷題確實能鍛鍊我們的程式設計能力,相信每個認真刷題的人都會有體會。現在提供線上程式設計評測的平臺有很多,比較有名的有 hihocoderLintCode,以及這裡我們關注的 LeetCode

程式碼提交曲線

LeetCode 是一個非常棒的 OJ(Online Judge)平臺,收集了許多公司的面試題目。相對其他 OJ 平臺而言,有著下面的幾個優點:

  • 題目全部來自業內大公司的真實面試
  • 不用處理輸入輸出,精力全放在解決具體問題上
  • 題目有豐富的討論,可以參考別人的思路
  • 精確瞭解自己程式碼在所有提交程式碼中執行效率的排名
  • 支援多種主流語言:C/C++,Python, Java
  • 可以線上進行測試,方便除錯

下面是我刷 LeetCode 的一些收穫,希望能夠引誘大家有空時刷刷題目。

問題:抽象思維

波利亞用三本書:《How To Solve It》、《數學的發現》、《數學與猜想》)來試圖闡明人類解決問題的一般性的思維方法,總結起來主要有以下幾種:

  • 時刻不忘未知量。即時刻別忘記你到底想要求什麼,問題是什麼。(動態規劃中問題狀態的設定)
  • 試錯。對題目這裡捅捅那裡搗搗,用上所有的已知量,或使用所有你想到的操作手法,嘗試著看看能不能得到有用的結論,能不能離答案近一步(回溯演算法中走不通就回退)。
  • 求解一個類似的題目。類似的題目也許有類似的結構,類似的性質,類似的解方案。通過考察或回憶一個類似的題目是如何解決的,也許就能夠借用一些重要的點子(比較 Ugly Number 的三個題目:263. Ugly Number264. Ugly Number II313. Super Ugly Number)。
  • 用特例啟發思考。通過考慮一個合適的特例,可以方便我們快速尋找出一般問題的解。
  • 反過來推導。對於許多題目而言,其要求的結論本身就隱藏了推論,不管這個推論是充分的還是必要的,都很可能對解題有幫助。

刷 LeetCode 的最大好處就是可以鍛鍊解決問題的思維能力,相信我,如何去思考本身也是一個需要不斷學習和練習的技能。

此外,大量高質量的題目可以加深我們對電腦科學中經典資料結構的深刻理解,從而可以快速用合適的資料結構去解決現實中的問題。我們看到很多ACM大牛,拿到題目後立即就能想出解法,大概就是因為他們對於各種資料結構有著深刻的認識吧。LeetCode 上面的題目涵蓋了幾乎所有常用的資料結構:

  • Stack:簡單來說具有後進先出的特性,具體應用起來也是妙不可言,可以看看題目 32. Longest Valid Parentheses
  • Linked List:連結串列可以快速地插入、刪除,但是查詢比較費時(具體操作連結串列時結合圖會簡單很多,此外要注意空節點)。通常連結串列的相關問題可以用雙指標巧妙的解決,160. Intersection of Two Linked Lists 可以幫我們重新審視連結串列的操作。
  • Hash Table:利用 Hash 函式來將資料對映到固定的一塊區域,方便 O(1) 時間內讀取以及修改。37. Sudoku Solver 數獨是一個經典的回溯問題,配合 HashTable 的話,執行時間將大幅減少。
  • Tree:樹在計算機學科的應用十分廣泛,常用的有二叉搜尋樹,紅黑書,B+樹等。樹的建立,遍歷,刪除相對來說比較複雜,通常會用到遞迴的思路,113. Path Sum II 是一個不錯的開胃菜。
  • Heap:特殊的完全二叉樹,“等級森嚴”,可以用 O(nlogn) 的時間複雜度來進行排序,可以用 O(nlogk) 的時間複雜度找出 n 個數中的最大(小)k個,具體可以看看 347. Top K Frequent Elements

演算法:時間空間

我們知道,除了資料結構,具體演算法在一個程式中也是十分重要的,而演算法效率的度量則是時間複雜度和空間複雜度。通常情況下,人們更關注時間複雜度,往往希望找到比 O( n^2 ) 快的演算法,在資料量比較大的情況下,演算法時間複雜度最好是O(logn)或者O(n)。計算機學科中經典的演算法思想就那麼多,LeetCode 上面的題目涵蓋了其中大部分,下面大致來看下。

當然,還有一部分問題可能需要一些數學知識去解決,或者是需要一些位運算的技巧去快速解決。總之,我們希望找到時間複雜度低的解決方法。為了達到這個目的,我們可能需要在一個解題方法中融合多種思想,比如在 300. Longest Increasing Subsequence 中同時用到了動態規劃和二分查詢的方法,將複雜度控制在 O(nlogn)。如果用其他方法,時間複雜度可能會高很多,這種題目的執行時間統計圖也比較有意思,可以看到不同解決方案執行時間的巨大差異,如下:

當然有時候我們會犧牲空間換取時間,比如在動態規劃中狀態的儲存,或者是記憶化搜尋,避免在遞迴中計算重複子問題。213. House Robber II一個Discuss會教我們如何用記憶化搜尋減少程式執行時間。

語言:各有千秋

對一個問題來說,解題邏輯不會因程式語言而不同,但是具體coding起來語言之間的差別還是很大的。用不同語言去解決同一個問題,可以讓我們更好地去理解語言之間的差異,以及特定語言的優勢。

速度 VS 程式碼量

C++ 以高效靈活著稱,LeetCode 很好地印證了這一點。對於絕大多數題目來說,c++ 程式碼的執行速度要遠遠超過 python 以及其他語言。和 C++ 相比,Python 允許我們用更少的程式碼量實現同樣的邏輯。通常情況下,Python程式的程式碼行數只相當於對應的C++程式碼的行數的三分之一左右。

347 Top K Frequent Elements 為例,給定一個陣列,求陣列裡出現頻率最高的 K 個數字,比如對於陣列 [1,1,1,2,2,3],K=2 時,返回 [1,2]。解決該問題的思路比較常規,首先用 hashmap 記錄每個數字的出現頻率,然後可以用 heap 來求出現頻率最高的 k 個數字。

如果用 python 來實現的話,主要邏輯部分用兩行程式碼就足夠了,如下:

num_count = collections.Counter(nums)
return heapq.nlargest(k, num_count, key=lambda x: num_count[x])

當然了,要想寫出短小優雅的 python 程式碼,需要對 python 思想以及模組有很好的瞭解。關於 python 的相關知識點講解,可以參考這裡

而用 C++ 實現的話,程式碼會多很多,帶來的好處就是速度的飛躍。具體程式碼在這裡,建立大小為 k 的小頂堆,每次進堆時和堆頂進行比較,核心程式碼如下:

// Build the min-heap with size k.
for(auto it = num_count.begin(); it != num_count.end(); it++){
  if(frequent_heap.size() < k){
      frequent_heap.push(*it);
  }
  else if(it->second >= frequent_heap.top().second){
      frequent_heap.pop();
      frequent_heap.push(*it);
  }
}

語言的差異

我們都知道 c++ 和 python 是不同的語言,它們有著顯著的區別,不過一不小心我們就會忘記它們之間的差別,從而寫出bug來。不信?來看 69 Sqrt(x),實現 int sqrt(int x)。這題目是經典的二分查詢(當然也可以用更高階的牛頓迭代法),用 python 來實現的話很容易寫出 AC 的程式碼

如果用 C++ 的話,相信很多人也能避開求中間值的整型溢位的坑:int mid = low + (high - low) / 2;,於是寫出下面的程式碼:

int low = 0, high = x;
while(low <= high){
  // int mid = (low+high) / 2,  may overflow.
  int mid = low + (high - low) / 2;
  if(x>=mid*mid && x<(mid+1)*(mid+1)) return mid;
  else if(x < mid*mid)  high = mid - 1;
  else low = mid + 1;
}

很可惜,這樣的程式碼仍然存在整型溢位的問題,因為mid*mid 有可能大於 INT_MAX,正確的程式碼在這裡。當我們被 python 的自動整型轉換寵壞後,就很容易忘記c++整型溢位的問題。

除了臭名昭著的整型溢位問題,c++ 和 python 在位運算上也有著一點不同。以 371 Sum of Two Integers 為例,不用 +, - 實現 int 型的加法 int getSum(int a, int b)。其實就是模擬計算機內部加法的實現,很明顯是一個位運算的問題,c++實現起來比較簡單,如下:

int getSum(int a, int b) {
    if(b==0){
        return a;
    }
    return getSum(a^b, (a&b)<<1);
}

然而用 python 的話,情況變的複雜了很多,歸根到底還是因為 python 整型的實現機制,具體程式碼在這裡

討論:百家之長

如果說 LeetCode 上面的題目是一塊塊金子的話,那麼評論區就是一個點綴著鑽石的礦山。多少次,當你絞盡腦汁終於 AC,興致勃發地來到評論區準備吹水。結果迎接你的卻是大師級的程式碼。於是,你高呼:尼瑪,竟然可以這樣!然後閉關去思考那些優秀的程式碼,順便默默鄙視自己。

除了優秀的程式碼,有時候還會有直觀的解題思路分享,方便看看別人是如何解決這個問題的。@MissMary在“兩個排序陣列中找出中位數”這個題目中,給出了一個很棒的解釋:Share my o(log(min(m,n)) solution with explanation,獲得了400多個贊。

你也可以評論大牛的程式碼,或者提出改進方案,不過有時候可能並非如你預期一樣改進後程式碼會執行地更好。在 51. N-Queens 的討論 Accepted 4ms c++ solution use backtracking and bitmask, easy understand 中,@binz 在討論區中納悶自己將陣列 vector<int> (取值非零即一)改為 vector<bool> 後,執行時間變慢。@prime_tang 隨後就給出建議說最好不要用 vector<bool>,並給出了兩個 StackOverflow 答案

當你逛討論區久了,你可能會有那麼一兩個偶像,比如@StefanPochmann。他的一個粉絲 @agave 曾經問 StefanPochmann 一個問題:

Hi Stefan, I noticed that you use a lot of Python tricks in your solutions, like "v += val," and so on... Could you share where you found them, or how your learned about them, and maybe where we can find more of that? Thanks!

StefanPochmann 也不厭其煩地給出了自己的答案:

@agave From many places, though I'd say I learned a lot on CheckiO and StackOverflow (when I was very active there for a month). You might also find some by googling python code golf.

原來大神也是在 StackOverflow 上修煉的,看來需要在 為什麼離不開 StackOverflow 中新增一個理由了:因為 StefanPochmann 都混跡於此。

類似這樣友好,充滿技術味道的討論,在 LeetCode 討論區遍地都是,絕對值得我們去好好探訪。

成長:大有益處

偶爾會聽旁邊人說 XX 大牛 LeetCode 刷了3遍,成功進微軟,還拿了 special offer!聽起來好像刷題就可以解決工作問題,不過要知道還有刷5遍 LeetCode 仍然沒有找到工作的人呢。所以,不要想著刷了很多遍就可以找到好工作,畢竟比你刷的還瘋狂的大有人在(開個玩笑)。

不過,想想前面列出的那些好處,應該值得大家抽出點時間來刷刷題了吧。



作者:selfboot
連結:https://www.jianshu.com/p/7bfbaf893a34
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。

相關文章