1. 程式人生 > >LeetCode 56,57,60,連刷三題不費勁

LeetCode 56,57,60,連刷三題不費勁

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注
今天是**LeetCode專題的第34篇**文章,剛好接下來的題目比較簡單,很多和之前的做法類似。所以我們今天出一個合集,一口氣做完接下來的57、59和60這三題。 再次申明一下,為了節約篇幅,保證文章的質量,我跳過了LeetCode當中所有的Easy以及少量沒什麼營養的Medium和Hard的問題。Easy的問題都不是很難,即使是新手一般來說也不需要看題解,仔細想想也應該能搞定。所以就不佔用篇幅了,如果大家有比較感興趣的Easy問題,可以在下方的小程式處給我留言。
## LeetCode 57 插入區間
第一題是57題Insert Interval,插入區間。題意會給定一組區間和一個單獨的區間,要求**將這個單獨的區間插入區間集合**,如果有兩個區間存在交叉的情況,需要將它們合併,要求合併之後的最終結果。從題意上來看,基本上和我們上一篇文章講的區間合併問題完全一樣。唯一不同的是,在這題當中給定的這一組區間都是**天然有序**的,我們不需要對它進行排序。 區間已經有序了,剩下的就很簡單了,我們只需要進行插入即可。區間插入的判斷條件還是和之前一樣,如果A區間的左端點在B區間左端點左側,那麼只要A區間的右側端點在B區間左端點的右側即可。所以這題基本上沒有難度,就是一道裸題,我也不明白為什麼官方給定的難度是Hard。 我們直接放出程式碼: ```python class Solution: def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]: ret = [] # l, r記錄待插入區間 l, r = newInterval # 記錄待插入區間是否完成插入 flag = False for x, y in intervals: # x, y記錄當前區間 # 如果當前區間在待插入區間左側,那麼將當前區間插入答案 if y < l: ret.append([x, y]) # 如果當前區間在待插入區間右側,那麼將兩個區間都插入答案 elif r < x: if not flag: flag = True ret.append([l, r]) ret.append([x, y]) # 否則,說明當前區間與待插入區間可以合併 # 更新待插入區間的範圍 else: l, r = min(l, x), max(r, y) # 如果最後還沒有完成插入,說明待插入區間大於所有區間 # 手動插入,防止遺漏 if not flag: ret.append([l, r]) return ret ``` 只要理解了區間合併的條件,這題真的沒有難度。
## LeetCode 59 螺旋矩陣II
前不久我們剛出過螺旋矩陣I的題解,在螺旋矩陣I當中,我們給定了一個矩陣讓我們螺旋形去遍歷它。這題則相反,給定我們矩陣的長和寬,讓我們**生成一個這樣的螺旋矩陣**。 我們來看下樣例: ![](https://user-gold-cdn.xitu.io/2020/5/4/171dfdf8b24615b0?w=678&h=304&f=jpeg&s=9511) 在這題當中,我們使用54題的思路也完全可以解出來,但是這題更加簡單了一些。由於是讓我們構造一個矩陣,那麼我們其實沒有必要維護每個方向的邊界了。只要出現**出界或者是遇到了已經填好的數字**那麼就說明應該轉向了。某種程度上來說,這題應該是I,之前的54題應該是II,因為這題更簡單一些。 如果對54題解法不熟悉的同學,可以點選下方的傳送門,學習一下方向陣列的使用方法。 [LeetCode54 螺旋矩陣,題目不重要,重要的是這個技巧](https://mp.weixin.qq.com/s?__biz=MzUyMTM5OTM2NA==&mid=2247485148&idx=1&sn=367f88b8e4863ef2dd57f50a0c931004&chksm=f9dafbf7cead72e16eed47c1aac3cc61894d2f87f2abdcfb3c21eaa036afc9bff1f2206108a4&scene=21#wechat_redirect) 由於我們不需要維護每個方向的邊界,並且移動的步數是固定的,更重要的是,**轉向每次最多隻會發生一次**,所以這個流程非常簡單,我們直接來看程式碼即可。 ```python class Solution: def generateMatrix(self, n: int) -> List[List[int]]: # 初始化答案 ret = [[0 for _ in range(n)] for _ in range(n)] # 初始化方向陣列 fx = [[0, 1], [1, 0], [0, -1], [-1, 0]] # 初始化方向以及起始點 dt = 0 x, y = 0, 0 # n*n的方陣,一共需要填入n*n個數字 for i in range(n*n): ret[x][y] = i+1 # 移動的下一個位置 x_, y_ = x+fx[dt][0], y+fx[dt][1] # 如果發生超界或者是遇到的數字大於0,說明需要轉向 if x_ < 0 or x_ >= n or y_ < 0 or y_ >= n or ret[x_][y_] > 0: dt = (dt + 1) % 4 # 轉向之後的位置 x, y = x+fx[dt][0], y+fx[dt][1] return ret ```
## LeetCode 60 第K個排列
這題是一個排列組合問題,給定一個整數n,它代表[1,2,3,...,n]這n個元素的序列。然後還有一個整數K,要求這n個元素從小到大第K個排列。 這題其實蠻有意思,我覺得可以上hard,但遺憾的是它有一個討巧的辦法,大概也許是這樣,所以才被降級成Medium的吧。這個討巧的辦法應該蠻容易想到的,很明顯,由於n個數字是確定的,所以最小的排列一定是[1,2,3,...,n]。而我們之前做過一道LeetCode31題,它求的是給定一個排列,然後生成字典序比它大剛好一位的下一個排列。 既然如此,我們可以**重複使用這個演算法K-1次**,就得到了答案了。對於做過31題的同學而言,這題毫無難度。如果對於31題解法不熟悉的同學可以點選下方傳送門,回去複習一下。 [LeetCode 31:遞迴、回溯、八皇后、全排列一篇文章全講清楚](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzUyMTM5OTM2NA%3D%3D%26mid%3D2247484662%26idx%3D1%26sn%3D92f6f8ded2e1fa4e61e2cceeb13cf451%26chksm%3Df9daf9ddcead70cb9877b585a38bc0d87a6769b5de06549abb64779d150b8b5fa62b3574c439%26scene%3D21%23wechat_redirect) 但其實可以不用這麼麻煩,因為Python當中有自帶的排列組合的工具庫,我們可以直接呼叫,只用5行程式碼就可以搞定。 ```python class Solution: # 引入包 from itertools import permutations def getPermutation(self, n: int, k: int) -> str: # 由於最後返回的結果要是string # 生成['1','2','3',...,'n']的序列用來計算下一個排列 base = [str(i) for i in range(1, n+1)] # 呼叫permutations會得到一個按順序計算排列的生成器 perms = permutations(base, n) for i in range(k): # 我們呼叫k-1次next就能獲得答案了 ret = next(perms) return ''.join(ret) ``` 這個方法雖然寫起來短平快,但是也有一個重大的問題,就是**耗時很長**。這個其實很容易算,根據當前排列生成下一個排列的複雜度是$O(n)$。我們一共需要計算k-1次,所以整體的複雜度就是$O(nk)$,極端情況下k=n!,所以最差複雜度是$n \cdot n!$。要知道n個物體的排列一共有n!種,如果k很大,這個複雜度是爆表的。不過這題沒有過多為難人,這樣的複雜度也能AC。我個人覺得這是一個很遺憾的事情,因為簡單的演算法也可以AC會導致很多人沒有鬥志再去研究複雜的演算法了。 最後,我們來看正解。 正解其實**不涉及任何新的演算法和資料結構**,甚至說穿了一文不值,但是很多人就是很難想到。還是老話題,它需要我們對問題進行深入的思考。 既然我們一個一個地求下一個排列非常慢,那麼我們能不能有快速一點的辦法呢?加快速度大概有兩種辦法,第一種**增加步幅**,比如之前的方法是每次獲取字典序+1的排列, 我們能不能生成字典序+k的排列?第二種想法是我們能不能**直接求解答案**,直接生成出這個排列? 簡單分析一下會發現第一種是不可行的,或者說是偽命題。因為如果說我們可以想出一個求解字典序+k的演算法,那麼我們令這個k等於題目中要求的k不就是直接求解答案了?所以說,如果可能存在更好的辦法,一定只能是第二種,也就是直接構造答案的方法。那麼問題就剩下了,我們怎麼直接構造這個答案呢?這就需要我們對排列組合的理解了。 如果你親手寫過某個集合的全排列,你會發現一些規律。比如說123的全排列好了。我們都知道,它的全排列一共是123,132,213,231,31和321。這個很簡單,我們觀察一下會發現,123一共三個數字,6種排列。每個數字打頭的都是2種,如果換成1234的排列呢?列一下就會知道是6種。如果你找規律你會發現,每個數字開頭的種數是(n-1)!。 如果要推導也很簡單,因為**每個數字都是公平的**,所以每個數字開頭的種數都是一樣的。而全排列一共有n!種,所以分攤到每個數字頭上剩下(n-1)!種。那如果我們已經知道了第一個數字是1,請問第二個數字是2的種類數有多少種?同樣的方法可以算出是(n-2)!種。到這裡有沒有火花閃過的感覺? 我們來舉個例子吧,假設我們現在n=5,我們算一下會知道,一共有120種排列。假設我們要求第100個排列,由於從0開始,所以也就是第99大的排列。那麼根據我們剛才說的,我們已經知道每個數字開頭的情況都是4!也就是24種,那麼我們用99/24得到4,所以開頭的數字是第4大的數(第0大的是1),也就是5。答案一下子縮小了很多,那接下來呢?接下來我們用99減去5之前的所有情況,也就是96種,得到3。也就說答案是5開頭的第3個排列,那麼我再問,第二個數字是多少? 同樣的辦法,除去5之後還剩下4個數字,每個數字排第二都有3!也就是6種,我們用3/6=0,應該是第0大的數,也就是1。我們繼續,除去5和1之後,還剩3個數字。每個數字排第三的情況有2種,我們用3/2=1,我們應該選擇第1大的數,這裡剩下的數是2,3,4,所以第三位應該是3。以此類推,我們可以得到每一位的數字。整體的複雜度是O(n),和上面的方法相比,有了質的突破。 我把整個計算過程做成了圖,有看不懂的小夥伴可以結合一下下圖理解: ![](https://user-gold-cdn.xitu.io/2020/5/4/171dfdfe4a177af3?w=1461&h=1080&f=jpeg&s=57262) 最後,我們把這個方法實現即可: ```python class Solution: def getPermutation(self, n: int, k: int) -> str: frac = [1 for _ in range(n+1)] # 生成一個序列存放所有的元素 nums = [i for i in range(1, n+1)] # 計算每一位的種類數 for i in range(1, n): frac[i] = i * frac[i-1] ret = '' k -= 1 for i in range(n-1, -1, -1): # 計算第i位的數字是當前第幾大的數 cur = k // frac[i] # 放完之後取模,表示去除掉之前的所有情況數 k %= frac[i] # 求出當前元素之後,需要從序列當中移除 ret += str(nums[cur]) nums.remove(nums[cur]) return ret ```
## 結尾
到這裡三題就算是講完了,今天的這三道題目或多或少的都和之前的問題有關,這也是我把這三題放在一篇文章當中闡述的原因。 這三題當中我個人最喜歡第三題,尤其是完美解法。它的整個思路和程式碼都不復雜,也沒有什麼特殊的技巧或者是方法,但是如果沒有對題目有足夠深入的瞭解是很難想到這個演算法的。這其實也是演算法題的精髓所在,比賽當中多的是知道解法也做不出來的題目。所以我們要提升演算法水平,光學演算法是不夠的,**也需要對題目有深入理解**才行。 今天的文章就到這裡,原創不易,需要你的**一個關注**,你的舉手之勞對我來說很重要。 ![](https://user-gold-cdn.xitu.io/2020/5/4/171dfdfb4f408ad3?w=258&h=258&f=png&