歡迎加入伯樂在線 專欄作者。
雖然刷題一直飽受詬病,不過不可否認刷題確實能鍛煉我們的編程能力,相信每個認真刷題的人都會有體會。現在提供在線編程評測的平臺有很多,比較有名的有 hihocoder,LintCode,以及這裏我們關註的 LeetCode。
LeetCode 是一個非常棒的 OJ(Online Judge)平臺,收集了許多公司的面試題目。相對其他 OJ 平臺而言,有著下面的幾個優點:
- 題目全部來自業內大公司的真實面試
- 不用處理輸入輸出,精力全放在解決具體問題上
- 題目有豐富的討論,可以參考別人的思路
- 精確了解自己代碼在所有提交代碼中運行效率的排名
- 支持多種主流語言:C/C++,python, Java
- 可以在線進行測試,方便調試
下面是我刷 LeetCode 的一些收獲,希望能夠引誘大家有空時刷刷題目。
問題:抽象思維
波利亞用三本書:《How To Solve It》、《數學的發現》、《數學與猜想》)來試圖闡明人類解決問題的一般性的思維方法,總結起來主要有以下幾種:
時刻不忘未知量
。即時刻別忘記你到底想要求什麽,問題是什麽。(動態規劃中問題狀態的設定)試錯
。對題目這裏捅捅那裏搗搗,用上所有的已知量,或使用所有你想到的操作手法,嘗試著看看能不能得到有用的結論,能不能離答案近一步(回溯算法中走不通就回退)。求解一個類似的題目
。類似的題目也許有類似的結構,類似的性質,類似的解方案。通過考察或回憶一個類似的題目是如何解決的,也許就能夠借用一些重要的點子(比較 Ugly Number 的三個題目:263. Ugly Number, 264. Ugly Number II, 313. 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 上面的題目涵蓋了其中大部分,下面大致來看下。
- 分而治之:有點類似“大事化小、小事化了”的思想,經典的歸並排序和快速排序都用到這種思想,可以看看 Search a 2D Matrix II 來理解這種思想。
- 動態規劃:有點類似數學中的歸納總結法,找出狀態轉移方程,然後逐步求解。 309. Best Time to Buy and Sell Stock with Cooldown 是理解動態規劃的一個不錯的例子。
- 貪心算法:有時候只顧局部利益,最終也會有最好的全局收益。122. Best Time to Buy and Sell Stock II 看看該如何“貪心”。
- 搜索算法(深度優先,廣度優先,二分搜索):在有限的解空間中找出滿足條件的解,深度和廣度通常比較費時間,二分搜索每次可以將問題規模縮小一半,所以比較高效。
- 回溯:不斷地去試錯,同時要註意回頭是岸,走不通就換條路,最終也能找到解決問題方法或者知道問題無解,可以看看 131. Palindrome Partitioning。
當然,還有一部分問題可能需要一些數學知識去解決,或者是需要一些位運算的技巧去快速解決。總之,我們希望找到時間復雜度低的解決方法。為了達到這個目的,我們可能需要在一個解題方法中融合多種思想,比如在 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 來實現的話,主要邏輯部分用兩行代碼就足夠了,如下:
12 num_count = collections.Counter(nums)return heapq.nlargest(k, num_count, key=lambda x: num_count[x])當然了,要想寫出短小優雅的 python 代碼,需要對 python 思想以及模塊有很好的了解。關於 python 的相關知識點講解,可以參考這裏。
而用 C++ 實現的話,代碼會多很多,帶來的好處就是速度的飛躍。具體代碼在這裏,建立大小為 k 的小頂堆,每次進堆時和堆頂進行比較,核心代碼如下:
123456789 // Build the min-heap with size k.for(auto it = num_count.begin(); it != num_count.end(); it++){ if(frequent_heap.size() 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;
,於是寫出下面的代碼:
很可惜,這樣的代碼仍然存在整型溢出的問題,因為mid*mid 有可能大於 INT_MAX
,正確的代碼在這裏。當我們被 python 的自動整型轉換寵壞後,就很容易忘記c++整型溢出的問題。
除了臭名昭著的整型溢出問題,c++ 和 python 在位運算上也有著一點不同。以 371 Sum of Two Integers 為例,不用 +, – 實現 int 型的加法 int getSum(int a, int b)
。其實就是模擬計算機內部加法的實現,很明顯是一個位運算的問題,c++實現起來比較簡單,如下:
然而用 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 (取值非零即一)改為 vector 後,運行時間變慢。@prime_tang 隨後就給出建議說最好不要用 vector,並給出了兩個 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 仍然沒有找到工作的人呢。所以,不要想著刷了很多遍就可以找到好工作,畢竟比你刷的還瘋狂的大有人在(開個玩笑)。
不過,想想前面列出的那些好處,應該值得大家抽出點時間來刷刷題了吧。
更多閱讀
跟波利亞學解題
為什麽我反對純算法面試題
聊聊刷題
如何看待中國學生為了進 Google、微軟等企業瘋狂地刷題?
LeetCode 編程訓練
國內有哪些好的刷題網站?
打賞支持我寫出更多好文章,謝謝!
打賞作者
打賞支持我寫出更多好文章,謝謝!
任選一種支付方式
Tags: Online 在線 Java 點子 動態
文章來源: