LeetCode刷題筆記【二】

#046 全排列

https://leetcode-cn.com/problems/permutations/

題目考察回溯思想

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        if len(nums) == 0:
            return []
        used_set = set()  # 保存排列過程中已經使用過的元素
        outs = []
        self.backtrack(nums, 0, [], used_set, outs)
        return outs

    def backtrack(self, nums, index, pres, used_set, outs):
        # 終止條件
        if index == len(nums):
            outs.append(pres[:])
            return

        for i in range(len(nums)):
            # 前面使用過的元素不再使用
            if nums[i] not in used_set:
                used_set.add(nums[i])
                pres.append(nums[i])
                self.backtrack(nums, index + 1, pres, used_set, outs)
                # 狀態重置
                used_set.remove(nums[i])
                pres.pop()

以對nums=[3 1 2]全排列爲例,畫出其遞歸樹:

  1. 調用backtrack(),遞歸終止沒有終止則繼續執行
  2. 當前節點有哪些路徑可選:循環選則,初始節點3 2 1均可選,第一次循環選則nums[0]=3
  3. 已選元素標記佔用:將nums[0]添加到used_set,標記nums[0]佔用,將選則的元素添加到pres
  4. 調用backtrack()進入下一個節點:(當前節點有哪些路徑可選:循環選則,nums[0]標記爲佔用,1 2可選,第一次循環選則nums[1]=1;將選則的元素添加到pres,將nums[1]添加到used_set,標記nums[1]佔用;調用backtrack()進入下一個節點。(重複1234步,即一直向下,遞歸到更深的節點))
  5. 直到滿足終止條件,退出遞歸,返回上一節點(回溯):執行狀態重置下面的代碼,解除對元素的佔用,繼續上一層節點的循環選則

#053 最大子序和

https://leetcode-cn.com/problems/maximum-subarray/

題目考察動態規劃思想

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        max_sum = nums[0]
        local_sum = 0
        for curr_num in nums:
            if local_sum > 0:  # 以上一個節點爲結尾的所有子序列中最大的和
                local_sum += curr_num  # 以當前節點爲結尾的所有子序列中最大的和
            else:
                local_sum = curr_num  
            # 狀態轉移max_sum[i] = max(max_sum[i-1], local_sum[i])
            max_sum = max(max_sum, local_sum)
        return max_sum

"""class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        # 暴力法求解超時O(n^2)
        max_sum = nums[0]
        for i in range(len(nums)):
            curr_sum = nums[i]
            if curr_sum >= max_sum:
                max_sum = curr_sum
            for j in range(i + 1, len(nums)):
                curr_sum += nums[j]
                if curr_sum >= max_sum:
                    max_sum = curr_sum
        return max_sum"""

暴力解法的思路是,遍歷以當前節點開頭的所有子序列,尋找和最大的子序;

換一個思路,遍歷以當前節點爲結尾的子序列,尋找最大子序該怎麼做呢?

這就可以劃分子問題:對序列[a b c d],假設以節點d爲結尾的子序列[... d]有最大和,那麼,當前節點定位到d:

  1. 要麼,以c爲結尾的子序列[... c]中存在和大於0的序列,最終的結果是max_sum=...+c+d
  2. 要麼,以c爲結尾的子序列[... c]的和都小於或等於0,最終的結果是max_sum=d
  3. 接着當前節點定位到c,繼續1 2過程,找出這個子序列

可以看出,上面的過程是一個遞歸的思想(自頂向下)。如果我們從最小的子問題開始,就變成了一個自下向上的動態規劃問題,從節點a開始,將當前節點視爲子序列的結尾:

  1. 初始狀態max_sum=a,local_sum=0(是以當前節點爲結尾的所有子序列中最大的和)
  2. 當前節點指向a
  3. 前一個節點爲null,以null爲結尾的子序列最大和是初始化的值local_sum=0
  4. local_sum不大於0(即local_sum不能使以下一個節點b爲結尾的子序列的和變大),捨棄local_sum,使它等於當前節點的值;local_sum>0,(即local_sum能使以下一個節點b爲結尾的子序列的和變大),local_sum加上當前節點a的值。此時的local_sum是以當前節點爲結尾的所有子序列中最大的和。
  5. 狀態轉移,max_sum[i] = max(max_sum[i-1], local_sum[i]),本次最大和max_sum等於上次最大和與本次local_sum之間更大的那一個;
  6. 當前節點指向b
  7. 前一個節點爲a,以a爲結尾的子序列最大和是local_sum
  8. 重複上述過程,直到得到最終狀態(以各個節點爲結尾的子序列中最大的和)

#056 合併區間

https://leetcode-cn.com/problems/merge-intervals/

題目考察合併區間的方法,先排序再合併,可以降低時間複雜度

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        if not intervals:
            return
        # 按每個區間的第一個元素對列表升序排序
        intervals.sort()
        outs = [intervals[0]]
        for interval in intervals[1:]:
            # 用[[x1, y1], [x2, y2], ...]
            # 按x排序後,相鄰區間,前一個區間的y大於等於後一個區間的x就可以合併
            if outs[-1][-1] >= interval[0]:
                outs[-1][-1] = max(interval[-1], outs[-1][-1])
            else:
                outs.append(interval)
        return outs

如果不排序,直接合並,需要對區間兩兩比較。

先按每個區間的x升序排序之後,相鄰區間,前一個區間的y大於等於後一個區間的x就可以合併,選擇前一個區間的x和兩個區間較大的y合併爲一個新的區間。

sort() 函數用於對原列表進行排序:list.sort( key=None, reverse=False)

  • key -- 是帶一個參數的函數,指定可迭代對象中的一個元素來進行排序。
  • reverse -- 排序規則,reverse = True 降序, reverse = False 升序(默認)。

例如對二維、三維列表排序:

test = [[1, 5], [4, 3], [2, 6]]
test.sort(key=lambda x: x[0])   # 按第一維每個區間的第一個元素排序
print(test)
test.sort(key=lambda x: x[1])  # # 按第一維每個區間的第二個元素排序排序
print(test)

test = [[[1, 5], [4, 3]], [[6, 6], [3, 4]], [[3, 4], [2, 4]]]
test.sort(key=lambda x: x[0])   # 按第二維每個區間的第一個元素的第一個元素排序x[0][0]
print(test)
test.sort(key=lambda x: x[1][0])  # 按第二維每個區間的第二個元素的第一個元素排序
print(test)

結果依次是:

[[1, 5], [2, 6], [4, 3]]
[[4, 3], [1, 5], [2, 6]]
[[[1, 5], [4, 3]], [[3, 4], [2, 4]], [[6, 6], [3, 4]]]
[[[3, 4], [2, 4]], [[6, 6], [3, 4]], [[1, 5], [4, 3]]]

#011 盛水最多的容器

https://leetcode-cn.com/problems/container-with-most-water/

class Solution:
    def maxArea(self, height: List[int]) -> int:
        # 雙指針,指向列表開始和結尾
        p_left, p_right = 0, (len(height) - 1)
        max_area = 0
        while p_left < p_right:
            curr_area = (p_right - p_left) * min(height[p_left], height[p_right])
            max_area = max(max_area, curr_area)
            if height[p_left] < height[p_right]:
                p_left += 1
            else:
                p_right -= 1
        return max_area

求a1到an任意兩邊與x軸圍成的面積,暴力解法依舊是雙循環遍歷所有組合。

注意到,使面積S最大,要使寬和高都最大。

  1. 首先最大的寬是雙指針的初始位置p_left和p_right,而矩形的高由a1、an中較小的高決定,如圖所示。
  2. 接着,要使S有可能變大,將指向較小高的指針向中移動,尋找一個更大的高。(如果移動指向較大高的指針圖中p_right,矩形寬變小,而高最大依舊是圖中p_left指向的值,面積不可能變大)

#020 有效括號

https://leetcode-cn.com/problems/valid-parentheses/

題目考察對棧先入後出思想的靈活應用

class Solution:
    def isValid(self, s: str) -> bool:
        if s is None:
            return True
        dict_c = {'(': ')', '[': ']', '{': '}', 'none': 'none'}
        strs = []
        for char in s:
            if char in dict_c:
                strs.append(char)
            else:
                if dict_c[strs.pop() if strs else 'none'] is not char:
                    return False
        return not strs

括號的匹配規則恰好是:從左至右,在檢索到右括號之前,最後出現的左括號,匹配第一個右括號,而最出現的左括號,最後匹配右括號。與棧後入先出的規則類似。

#055 跳躍遊戲

https://leetcode-cn.com/problems/jump-game/

題目考察對貪心規則的理解,能夠結合問題特徵巧妙確定貪心規則。

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        bound = 0  # 可以跳到的最遠位置
        for index in range(len(nums)):
            # 如果當前位置index超出了可以跳到的最遠位置,則失敗
            if index > bound:
                return False
            # 最遠位置更新規則是,“歷史最遠位置”與“當前節點可跳到的最遠位置”的較大者
            bound = max(bound, index + nums[index])
        return True

從起始位置開始,每個索引都對應一個可以到達的最遠位置:index+nums[index];

那麼當前索引所能到達的最遠位置是:“已經走過的索引中能到達的最遠位置”bound和:“當前索引可以到達的最遠位置”中較大的那一個

設計貪心規則:

  • 可行性:如果當前索引超出了歷史記錄的可以到達的最遠位置,就失敗;
  • 局部最優:否則,選擇“歷史最遠位置”與“當前索引可跳到的最遠位置”的較大者,作爲本次記錄的歷史最遠位置,bound = max(bound, index + nums[index]);

從左至右遍歷列表,按上述規則,index可以到達列表末尾,則說明可以從開始跳到末尾。

#075 顏色分類

https://leetcode-cn.com/problems/sort-colors/

題目考察數組排序的方法

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        start = 0
        # 將0排好
        for i in range(len(nums)):
            if nums[i] == 0:
                nums[start], nums[i] = nums[i], nums[start]
                start += 1
        # 接着0之後的位置,排1
        for i in range(start, len(nums)):
            if nums[i] == 1:
                nums[start], nums[i] = nums[i], nums[start]
                start += 1
        return None

利用快速排序和雙指針的思想,先確定待分割的數字是0,遍歷列表,將所有是0的數字放在列表左側;

再確定待分割的數字是1,從0之後開始遍歷,將所有是1的數字放在0之後。

另一種思想是利用三指針,一次遍歷即可,如下:

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        start = 0  # 待交換爲0的位置
        end = len(nums) - 1  # 待交換爲2的位置
        update_pos = 0  # 當前位置
        while update_pos <= end:
            if nums[update_pos] == 0:
                nums[start], nums[update_pos] = nums[update_pos], nums[start]
                start += 1
                update_pos += 1
            elif nums[update_pos] == 2:
                nums[end], nums[update_pos] = nums[update_pos], nums[end]
                end -= 1
            # 當前位置是1,向後移動當前位置
            else:
                update_pos += 1

#078 子集

https://leetcode-cn.com/problems/subsets/

題目考察訪問和創建列表的方法,通過列表推導可以方便的創建列表

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = [[]]
        for num in nums:
            # 列表推導
            res += [([num] + old_num) for old_num in res]
        return res

列表推導創建子集就是:從前向後遍歷列表,每遇到一個新元素,就將這個新元素分別添加到每個已經得到的子集上,作爲一組新的子集加入到列表子集中,如圖所示:

class Solution:
    def subsets(self, nums: List[int]) -> List[List[int]]:
        res = []
        pre = []
        self.find_subset(nums, 0, pre, res)
        return res

    def find_subset(self, nums, start, pre, res):
        # 將每層遞歸得到的子集添加到結果列表
        res.append(pre[:])
        # 終止條件
        if start == len(nums):
            return
        # 從前向後遍歷每層列表
        for index in range(start, len(nums)):
            # 當前佔用(訪問過)的元素加入子集
            pre.append(nums[index])
            self.find_subset(nums, index + 1, pre, res)
            # 解除佔用,回溯
            pre.pop()

另一種方法是回溯的思想:

#051 N皇后

https://leetcode-cn.com/problems/n-queens/

題目考察回溯思想,是回溯思想的一個經典案例

class Solution:
    main_diags = set()  # 不可放置皇后的主對角線
    para_diags = set()  # 不可放置皇后的副對角線
    cols = set()  # 不可放置皇后的列

    def solveNQueens(self, n: int):
        res = []
        if n == 0:
            return res

        self.backtrack(0, n, [], res)
        return res

    def backtrack(self, row, size, pre, res):
        """遞歸 得到當前row的皇后放置位置"""
        # 若存在解,那麼每行必有一個皇后
        # 遞歸到終止條件,返回一個解
        if row == size:
            res.append(self.place_queen(pre, size))
            return
        for col in range(size):
            # 當前row的col列可以放置皇后
            if self.is_set_queen(row, col):
                pre.append(col)  # 有序(pre列表索引爲行號,對應元素爲列號)記錄放置皇后的位置
                self.main_diags.add(row - col)
                self.para_diags.add(row + col)
                self.cols.add(col)
                
                # 進入判斷下一行的皇后位置
                self.backtrack(row + 1, size, pre, res)
                # 回溯 恢復狀態
                # 釋放佔用的主副對角線和列
                self.cols.remove(col)
                self.para_diags.remove(row + col)
                self.main_diags.remove(row - col)

                pre.pop()

    def is_set_queen(self, row, col) -> bool:
        """(row,col)位置是否可以放置皇后"""
        if ((row - col) not in self.main_diags) and (
                (row + col) not in self.para_diags) and (
                col not in self.cols):
            return True
        else:
            return False

    def place_queen(self, pre, size):
        """放置一張圖上的皇后"""
        return ["." * pre[index] + "Q" + "." * (size - pre[index] - 1) for index in range(size)]

n皇后問題:在n×n格的國際象棋上擺放n個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。

滿足這樣的要求,要麼每行必定會有的一個皇后,得到一個解;要麼不存在解(例如n=2)

可以通過遞歸,從第一行row=0開始

  1. 遍歷本行的每一列,該列是否滿足放置皇后的條件
  2. 如果滿足,記錄這個位置,並將這個皇后佔據的列、主對角線、副對角線標記爲“不可放置”
  3. 遞歸進入下一行,執行1,2步,直到滿足遞歸終止條件(或一層遍歷結束),退出
  4. 退出時,回溯到上一次的狀態,解除本次對列、主對角線、副對角線的佔用

其中,如果Q在(r,c)位置,那麼col=c的列,row+col=r+c的副對角線,row-col=r-c的主對角線被佔用。

#059 螺旋矩陣Ⅱ

https://leetcode-cn.com/problems/spiral-matrix-ii/

題目考察對過程的模擬

class Solution:
    def generateMatrix(self, n: int) -> List[List[int]]:
        left, top, right, bottom = 0, 0, n-1, n-1  # 記錄左上右下的邊界
        step, all_step = 1, n*n  # 1-n*n的數字
        num_map = [[0 for _ in range(n)] for _ in range(n)]
        while step <= all_step:
            # 從左至右,在邊界範圍內填充一行,填充完畢上邊界下移
            for i in range(left, right+1):
                num_map[top][i] = step
                step += 1
            top += 1
            # 從上至下,在邊界範圍內填充一行,填充完畢右邊界左移
            for i in range(top, bottom+1):
                num_map[i][right] = step
                step += 1
            right -= 1
            # 從右至左,在邊界範圍內填充一行,填充完畢下邊界上移
            for i in range(right, left-1, -1):
                num_map[bottom][i] = step
                step += 1
            bottom -= 1
            # 從下至上,在邊界範圍內填充一行,填充完畢左邊界右移
            for i in range(bottom, top-1, -1):
                num_map[i][left] = step
                step += 1
            left += 1
        return num_map

先產生n*n的矩陣,按照填充過程,填入數字:

  1. 從左至右,在邊界範圍內填充一行,填充完畢上邊界下移
  2. 從上至下,在邊界範圍內填充一行,填充完畢右邊界左移
  3. 從右至左,在邊界範圍內填充一行,填充完畢下邊界上移
  4. 從下至上,在邊界範圍內填充一行,填充完畢左邊界右移
  5. 數字沒有填充完就返回第一步接着填充

#062 不同路徑

https://leetcode-cn.com/problems/unique-paths/

題目考察動態規劃,空間換時間的思想

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        d = [[1]*m] + [[1]+[0]*(m-1) for _ in range(n-1)]  # 存儲到達每個位置的路徑數,O(m*n)的空間複雜度
        for i in range(1, n):
            for j in range(1, m):
                d[i][j] = d[i-1][j] + d[i][j-1]
        return d[-1][-1]

動態規劃的關鍵在於將問題分解爲子問題,找到問題的狀態轉移方程。

  • 記d[i][j]爲一個狀態,表示可以到達(i,j)位置的路徑總數
  • 要到達(i,j)位置,可以從(i-1,j)右移一步或者(i,j-1)下移一步
  • 劃分子問題:到達(i,j)位置的路徑總數=到達(i-1,j)的路徑數與到達(i,j-1)位置的路徑數的和
  • 得到狀態轉移方程:d[i][j] = d[i-1][j] + d[i][j-1]

使用一維列表記憶,還可以降低空間複雜度

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        d = [1]*m  # O(m)空間複雜度
        for i in range(1, n):
            for j in range(1, m):
                d[j] = d[j] + d[j-1]
        return d[-1]

#094 二叉樹的中序遍歷

https://leetcode-cn.com/problems/binary-tree-inorder-traversal/

題目考察二叉樹的中序遍歷方法

# Definition for a binary tree node.
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None


class Solution:
    def inorderTraversal(self, root: TreeNode) -> List[int]:
        res = []
        stack = [(False, root)]  # 先加入頭結點,標記爲未訪問
        while stack:
            used, node = stack.pop()
            if node is None:
                continue
            # 當前節點沒有被訪問過,則將其右子節點、自身和左子節點加入棧
            if not used:
                stack.append((False, node.right))
                stack.append((True, node))  # 當前節點已訪問,標記爲True
                stack.append((False, node.left))
            else:
                res.append(node.val)
        return res

首先中序遍歷的意思是:對於當前結點,先遍歷它的左子樹,訪問根節點,然後輸出該節點,遍歷它的右子樹,訪問根節點。如下圖所示:

  • 1-->2-->4,4 的左子樹爲空,輸出 4,接着右子樹;
  • 6 的左子樹爲空,輸出 6,接着右子樹;
  • 7 的左子樹爲空,輸出 7,右子樹也爲空,此時 2 的左子樹全部輸出,輸出 2,2 的右子樹爲空,此時 1 的左子樹全部輸出,輸出 1,接着 1 的右子樹;
  • 3-->5,5 左子樹爲空,輸出 5,右子樹也爲空,此時 3 的左子樹全部輸出,而 3 的右子樹爲空,至此 1 的右子樹全部輸出,結束。

這種節點的訪問順序與棧先入後出一致,

  • 將節點1的右子節點3、節點1、左子節點2依次壓入棧,然後彈出節點2繼續這樣的遍歷過程,直到遇到子節點爲null,不再添加節點,彈出棧中的上一節點。
  • 將訪問過的節點標記爲True,未訪問的節點標記爲False
  • 如果彈出的節點是訪問過的,那麼將它加入中序遍歷結果列表,不再訪問它

#032最長有效括號

https://leetcode-cn.com/problems/longest-valid-parentheses/

題目考察棧的思想

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        maxlen = 0
        stack = [-1]  # 若從s[0]開始的括號都匹配,其長度爲最後一個括號的索引-(-1)
        for index in range(len(s)):
            if s[index] == '(':
                stack.append(index)
            elif s[index] == ')':
                stack.pop()
                # 棧空,從index=0開始的一組括號匹配完成;
                # 並且把下一組第一個不匹配的索引壓入棧底;
                # 作爲計算下一組括號可能的最大長度的基準
                if not stack:
                    stack.append(index)
                maxlen = max(maxlen, index-stack[-1])
        return maxlen
  • 對於遇到的每個"(" ,將它的下標放入棧中
  • 對於遇到的每個")",彈出棧頂的元素(即彈出與")"匹配的那個"(")
  • 並將當前元素的下標與剩餘的棧頂元素下標作差,得出當前有效括號字符串的長度

通過這種方法,我們繼續計算有效子字符串的長度,並最終返回最長有效子字符串的長度。

#091解碼方法

https://leetcode-cn.com/problems/decode-ways/

題目考察動態規劃的思想,類似爬樓梯問題

class Solution:
    def numDecodings(self, s: str) -> int:
        if not s or s[0] == '0':
            return 0
        pre, curr = 1, 1  # 動態規劃中上一次和上上一次的狀態(解碼的方法數)
        for i in range(1, len(s)):
            if s[i] == '0':
                # 直接字符比較,而不是轉換成整數,降低運行實間
                # if s[i-1] == '1' or (s[i-1] == '2' and s[i] <= '6'):
                if s[i-1] != '0' and int(s[i - 1])*10+int(s[i]) <= 26:
                    pre, curr = curr, pre
                else:
                    return 0
            else:
                if s[i-1] != '0' and int(s[i - 1])*10+int(s[i]) <= 26:
                    pre, curr = curr, curr + pre
                else:
                    pre, curr = curr, curr
        return curr

這個問題與爬樓梯問題(從起點開始,一次可以跨一級臺階或兩級臺階,求到最後一級臺階的行走方法)類似,只不過在解碼的每一步都多了一些條件需要判斷。

首先,s爲空或者s[0]=='0'是沒有解的,

1. s[i] == '0' 即當前數字不能單獨解碼

1)s[i-1]s[i] <= 26 但與前一個數組合可以解碼

pre, curr = curr, pre

2)與前一個數組合可不以解碼,解碼失敗

2. s[i] != '0' 當前數字本身可以解碼

1)s[i-1]s[i] <= 26 並且與前一個數組合也可以解碼

pre, curr = curr, curr + pre

2)與前一個數組合後就不可以解碼

pre, curr = curr, curr

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章