LeetCode 57.59.60,帶你一塊兒砍瓜切菜刷三題

本文始發於我的公衆號:TechFlow,原創不易,求個關注python


今天是LeetCode專題的第34篇文章,恰好接下來的題目比較簡單,不少和以前的作法相似。因此咱們今天出一個合集,一口氣作完接下來的5七、59和60這三題。算法

再次申明一下,爲了節約篇幅,保證文章的質量,我跳過了LeetCode當中全部的Easy以及少許沒什麼養分的Medium和Hard的問題。Easy的問題都不是很難,即便是新手通常來講也不須要看題解,仔細想一想也應該能搞定。因此就不佔用篇幅了,若是你們有比較感興趣的Easy問題,能夠在下方的小程序處給我留言。小程序


LeetCode 57 插入區間


第一題是57題Insert Interval,插入區間。題意會給定一組區間和一個單獨的區間,要求將這個單獨的區間插入區間集合,若是有兩個區間存在交叉的狀況,須要將它們合併,要求合併以後的最終結果。從題意上來看,基本上和咱們上一篇文章講的區間合併問題徹底同樣。惟一不一樣的是,在這題當中給定的這一組區間都是自然有序的,咱們不須要對它進行排序。數組

區間已經有序了,剩下的就很簡單了,咱們只須要進行插入便可。區間插入的判斷條件仍是和以前同樣,若是A區間的左端點在B區間左端點左側,那麼只要A區間的右側端點在B區間左端點的右側便可。因此這題基本上沒有難度,就是一道裸題,我也不明白爲何官方給定的難度是Hard。數據結構

咱們直接放出代碼:app

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當中,咱們給定了一個矩陣讓咱們螺旋形去遍歷它。這題則相反,給定咱們矩陣的長和寬,讓咱們生成一個這樣的螺旋矩陣學習

咱們來看下樣例:spa

在這題當中,咱們使用54題的思路也徹底能夠解出來,可是這題更加簡單了一些。因爲是讓咱們構造一個矩陣,那麼咱們其實沒有必要維護每一個方向的邊界了。只要出現出界或者是遇到了已經填好的數字那麼就說明應該轉向了。某種程度上來講,這題應該是I,以前的54題應該是II,由於這題更簡單一些。code

若是對54題解法不熟悉的同窗,能夠點擊下方的傳送門,學習一下方向數組的使用方法。

LeetCode54 螺旋矩陣,題目不重要,重要的是這個技巧

因爲咱們不須要維護每一個方向的邊界,而且移動的步數是固定的,更重要的是,轉向每次最多隻會發生一次,因此這個流程很是簡單,咱們直接來看代碼便可。

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:遞歸、回溯、八皇后、全排列一篇文章全講清楚

但其實能夠不用這麼麻煩,由於Python當中有自帶的排列組合的工具庫,咱們能夠直接調用,只用5行代碼就能夠搞定。

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),和上面的方法相比,有了質的突破。

我把整個計算過程作成了圖,有看不懂的小夥伴能夠結合一下下圖理解:

最後,咱們把這個方法實現便可:

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
複製代碼

結尾


到這裏三題就算是講完了,今天的這三道題目或多或少的都和以前的問題有關,這也是我把這三題放在一篇文章當中闡述的緣由。

這三題當中我我的最喜歡第三題,尤爲是完美解法。它的整個思路和代碼都不復雜,也沒有什麼特殊的技巧或者是方法,可是若是沒有對題目有足夠深刻的瞭解是很難想到這個算法的。這其實也是算法題的精髓所在,比賽當中多的是知道解法也作不出來的題目。因此咱們要提高算法水平,光學算法是不夠的,也須要對題目有深刻理解才行。

今天的文章就到這裏,原創不易,須要你的一個關注,你的舉手之勞對我來講很重要。

相關文章
相關標籤/搜索