用 Swift 來刷 leet code 吧 (1-20)

戴倉薯發表於2017-01-17

前言

為什麼要用 Swift 刷 leetcode?因為我有兩個想法,一是學 Swift 並且有機會練練,二是想把 leetcode 刷完。於是,這兩個想法就合二為一了,現在我以基本一天一道的速度在刷。

Swift 適合用來刷 leetcode 嗎?現在做了20多道題,我個人的意見是不適合。可能比純 C 好寫,但沒有主流的語言 java、python 好寫。

首先 Swift 這門語言效率不高,有些演算法拿別的語言過得去,拿 Swift 就會超時(雖然是跟 case 有關係,不過 Swift 效率確實不高)。其次,Swift 有很多麻煩的地方,尤其是字串處理上。它甚至都不能隨機訪問字串裡某個位置的字元……還得先轉成一個字元陣列,也就意味著凡是有字串的題的時間和空間複雜度都不會小於 O(n) 了。還有一些字串相關的 API 會影響效率,如果想讓程式碼簡潔就會超時…… 總之感覺如果是練習演算法,不用考慮這些因素是最好的,從這個角度來說,Swift 並不是最適合刷 leet code 的語言。但當然也是可行的,如果你有興趣,一起來刷吧。

我把我的解法放在了我的 Github上,逐漸更新。另外,Github 上還有一個非常全的題解,我也為它貢獻了一點點程式碼。

每道題的筆記

以下是 1-20 題我寫的簡單題解,卡住的時候可以來看看:

  1. Two Sum (Easy) 題解
    很簡單的 hash。一個小技巧是,對於每個數先看和為 target 所需的數是否已經在 dict 裡,如果已經在則直接返回,否則才把自身放進 dict 裡。這樣只需迴圈一次,不用先構建 hash、再遍歷,迴圈兩次。
    時間複雜度:O(n) 空間複雜度:O(n)

  2. Add Two Numbers (Medium) 題解
    簡單的單連結串列處理。考慮幾種情況:1. 兩個數位數相等,且最高位不需進位 2. 兩個數位數相等,且最高位需要進位 3. 兩個數位數不相等。
    有些人寫的時候會在結果的頭部先建立一個dummyval任意,真正的頭結點直接往dummy後面插。最後返回dummy -> next
    時間複雜度:O(n) 空間複雜度:O(1)

  3. Longest Substring Without Repeating Characters (Medium) 題解
    我用的方法是用一個 hash 記錄每個字母出現的index,然後把字串掃一遍。不出現重複時就往 hash 表裡填。出現重複時,從 hash 取出字母出現的 previousIndex,把從當前串開頭至previousIndex的字母都從 hash 中清掉。
    看到一個更好的方法,不需要存字母出現的index,出現重複時直接從當前串開頭至出現重複字母的位置清掉 hash 即可。這種情況下也不需要用Dictionary存 hash,只需用Set即可。
    本來 hash 需要的額外空間很小,但因為 swift 要遍歷字串中的字元必須把字元陣列存出來一份。所以空間複雜度為 O(n)。
    時間複雜度:O(n) 空間複雜度:O(n)

  4. Median of Two Sorted Arrays (Hard) 題解
    下面列出了兩個解法,其中 Solution2 是自己想出來的,也過了全部測試資料,但方法非常不簡潔。思路是從兩邊逼近中位數,取兩個數列的中點,可證明總有一個不能滿足第 k 大的條件。然後就調整這個數列。問題在於,有些情況可能會調整過頭。另外,還有這個數列已經到頭、調整不了的情況,此時就需要去調另一個數列。總的來說仍然是 log(m + n) 的,但程式碼非常長,原理也不夠清晰。
    Solution1 參考了別人的題解,每次兩個數列各取 k/2 處,小者在這個位置之前全都截斷。
    為啥 Solution1 就非常簡潔呢?最主要的問題在於,Solution1 是從一側逼近問題的,每次迭代都更靠近答案。Solution2 是從兩側往中間逼近,然而兩個數列並沒有二分查詢那麼好的特性,有可能兩個指標都在答案的同側,還要回頭找。
    另外,Solution1 利用了一個技巧,保證每次迭代時 nums1 都更短,不然交換。可以避免很多對稱的重複程式碼。
    在語言方面,可以看出 swift 裡if(...) {return ...}這種基本都用guard代替。
    時間複雜度:log(m+n) 空間複雜度:O(m+n)

  5. Longest palindromic Substring (Medium) 題解
    這個解法是 O(n^2) 的。DP,先搜長度為 1、為 2…… 至 n。之所以寫法很不簡潔,多出了許多臨時變數,完全是 swift 的鍋。謹記 swift 字串的特性,由於每一位字元長度不一定相等,它是不能隨機訪問的。也就是說,如果不存臨時變數,取某一位的字元、取字串的長度、擷取子串,全部都是 n 級別的…… 所以一再超時。
    感覺很坑的是,我之前寫作isPalidromeMatrix[startIndex][endIndex] = ...這樣就會超時,而if (...) {isPalidromeMatrix[startIndex][endIndex] = true}這樣就不會。只不過多賦值了一些 false……
    而且把if isPalidrome改成if isPalidromeMatrix[startIndex][endIndex],時間會長一倍。感覺資料量稍微一大,swift 效能問題真的挺嚴重。
    時間複雜度:O(n^2) 空間複雜度:O(n^2)
    這個題是有一個 O(n) 的演算法的。首先有暴搜的思路,就是以任何一位為中心往外擴充套件。O(n) 的演算法是在這個基礎上,利用迴文串的特性,存在一個子串那麼中心點兩側對稱,在此基礎上再往外搜即可。具體可見這篇文章

  6. ZigZag Conversion (Easy) 題解
    非常簡單的題,唯一的難點就是題目本身描述得不太清楚了。需要自己考慮 row = 1、2 的邊界情況。
    swift 有個stride函式用來處理 for step 的情況。
    時間複雜度:O(n) 空間複雜度:O(n)

  7. Reverse Integer (Easy) 題解
    這題本身很簡單,但感覺是道不錯的面試題。可以測試面試者是否考慮各種邊界情況,對溢位有沒有概念。
    測試用例對 swift 給的不對,只能用Int32.max。我是把負數統一歸成正數來計算,這樣判斷溢位的語句可以簡單點。
    時間複雜度:O(lgn) 空間複雜度:O(1)

  8. String to Integer (Easy) 題解
    這道題與其說是寫程式碼不如說是寫 case…… 一堆 case,真是一點懶都不能偷呀。
    時間複雜度:O(n) 空間複雜度:O(n)

  9. Palindrome Number (Easy) 題解
    很簡單的題,沒給出的條件是負數不算迴文數。有個 case 1000021 一開始做錯了。另外一開始寫了個遞迴,後來發現沒必要……
    時間複雜度:O(n) 空間複雜度:O(1)

  10. Regular Expression Matching (Hard) 題解
    評級為 hard,但感覺這題不難…… 就是遞迴一位一位往後讀,遇到 * 就分兩種情況,用盡這個 token 或者下輪接著用這個 token。
    一個問題就是直接遞迴會超時。需要先把正則式處理一下:

    1. aa 合併為 a*
    2. a.b 合併為 .(就是 . 前後所有的 x 全都去掉)。
      時間複雜度:O(n) 空間複雜度:O(n)
  11. Container With Most Water (Medium) 題解
    本來想的是搜尋加剪枝,搜以每個點做一端的。最壞情況 O(n^2),結果最後有兩組資料(就是最壞情況)過不去,超時。
    改成題解裡這樣,從兩邊往中間搜,結果變成 O(n) 了。想改成記憶化搜尋,發現很難。
    搜尋的順序果然還是非常重要!很多東西從後往前搜,從中間往兩邊搜,從兩邊往中間搜,就差得多了……
    時間複雜度:O(n) 空間複雜度:O(1)

  12. Integer to Roman (Medium) 題解
    很簡單的遞迴,沒什麼可說的。就是細心一點吧。
    看到有的題解是把 1000、900、500 都存起來,這樣確實快很多,因為不用考慮往左加的情況。另外非遞迴因為不用算 10 次冪可能略快一點。
    時間複雜度:O(lgn) 空間複雜度:O(1)

  13. Roman to Integer (Easy) 題解
    很簡單的題,沒啥可說的。分情況討論,簡單遞迴即可(我怎麼這麼喜歡遞迴……)
    看到一個題解的思路挺巧妙,倒著往前掃,比前一位大就往上加,沒前一位大就減掉。算是利用了羅馬數字一個很好的特性吧。
    不過想了想好像沒啥倒過來的必要啊…… 直接從前往後掃也是一樣 O O
    時間複雜度:O(n) 空間複雜度:O(n)

  14. Longest Common Prefix (Easy) 題解
    最簡單的字串處理,沒什麼可說的。
    時間複雜度:O(nm) 空間複雜度:O(nm)

  15. 3Sum (Medium) 題解
    我用的還是 hash 方法,先找第一個數、再找第二個數…… 去重的方法就是要求三個數的序關係,這樣就不會重了。
    看到一個題解用的是先排序,然後先定第一個數,第二個數左頭,第三個數右頭。然後大了挪小的、小了挪大的…… 這樣。算是另一種思路吧。
    時間複雜度:O(n^2) 空間複雜度:O(n)

  16. 3Sum Closest (Medium) 題解
    這時候就用上上一題的排序思路了。先排好序,然後指定第一個數從左往右,第二個為第一個數右邊(剩下區間的最左端),第三個數為最右端。然後小了把左邊的往右挪、大了把右邊的往左挪……
    時間複雜度:O(n^2) 空間複雜度:O(n)

  17. Letter Combinations of a Phone Number (Medium) 題解
    懶癌犯了…… 明明一道廣搜的題,又寫成“遞迴”了,僅僅是因為懶得開兩個陣列…… 是不是已經沒得救了 O O
    好吧,後面老實補了一個正常版本。然後發現“遞迴”版本快得多…… 完全想不到任何原因呀,明明“遞迴”版本無謂地構造了一些字尾字串,難道是拷貝陣列很慢?
    時間複雜度:O(3^n) 空間複雜度:O(3^n)

  18. 4Sum (Medium) 題解
    在 3Sum 基礎上的延伸,先排序,再迴圈前兩位,後兩位左右夾逼。聽說這樣會超時?然而並沒有,時間還在中位數左右。500多ms
    一些簡單的剪枝,比如夾逼時最小的兩個數都不夠小、最大的兩個數都不夠大,那也就沒必要繼續了。加了這些剪枝,雖然程式碼長了很多很多,但是嗖快嗖快的,只要 52 ms。一下 beats 100% submissions,好有成就感呀。
    時間複雜度:O(n^3) 空間複雜度:O(n)

  19. Remove Nth Node From End of List (Easy) 題解
    挺簡單的題,沒什麼可說的。只要兩個指標,第一個指向頭部,第二個指向第 n 個節點;然後把兩個指標同時往後挪,當第二個指標到尾部時,第一個指標指向的就是就是倒數第 n 個節點,把它去了就行了。
    時間複雜度:O(n) 空間複雜度:O(n)

  20. Valid Parentheses (Easy) 題解
    大概是棧的最簡單的一道題了。如果是左括號,push;是右括號,pop,如果不匹配返回 false。結束後如果棧空則返回 true,否則返回 false。
    時間複雜度:O(n) 空間複雜度:O(n)

相關文章