#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]全排列爲例,畫出其遞歸樹:
- 調用backtrack(),遞歸終止沒有終止則繼續執行
- 當前節點有哪些路徑可選:循環選則,初始節點3 2 1均可選,第一次循環選則nums[0]=3
- 已選元素標記佔用:將nums[0]添加到used_set,標記nums[0]佔用,將選則的元素添加到pres
- 調用backtrack()進入下一個節點:(當前節點有哪些路徑可選:循環選則,nums[0]標記爲佔用,1 2可選,第一次循環選則nums[1]=1;將選則的元素添加到pres,將nums[1]添加到used_set,標記nums[1]佔用;調用backtrack()進入下一個節點。(重複1234步,即一直向下,遞歸到更深的節點))
- 直到滿足終止條件,退出遞歸,返回上一節點(回溯):執行狀態重置下面的代碼,解除對元素的佔用,繼續上一層節點的循環選則
#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:
- 要麼,以c爲結尾的子序列[... c]中存在和大於0的序列,最終的結果是max_sum=...+c+d
- 要麼,以c爲結尾的子序列[... c]的和都小於或等於0,最終的結果是max_sum=d
- 接着當前節點定位到c,繼續1 2過程,找出這個子序列
可以看出,上面的過程是一個遞歸的思想(自頂向下)。如果我們從最小的子問題開始,就變成了一個自下向上的動態規劃問題,從節點a開始,將當前節點視爲子序列的結尾:
- 初始狀態max_sum=a,local_sum=0(是以當前節點爲結尾的所有子序列中最大的和)
- 當前節點指向a
- 前一個節點爲null,以null爲結尾的子序列最大和是初始化的值local_sum=0
- local_sum不大於0(即local_sum不能使以下一個節點b爲結尾的子序列的和變大),捨棄local_sum,使它等於當前節點的值;local_sum>0,(即local_sum能使以下一個節點b爲結尾的子序列的和變大),local_sum加上當前節點a的值。此時的local_sum是以當前節點爲結尾的所有子序列中最大的和。
- 狀態轉移,max_sum[i] = max(max_sum[i-1], local_sum[i]),本次最大和max_sum等於上次最大和與本次local_sum之間更大的那一個;
- 當前節點指向b
- 前一個節點爲a,以a爲結尾的子序列最大和是local_sum
- 重複上述過程,直到得到最終狀態(以各個節點爲結尾的子序列中最大的和)
#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最大,要使寬和高都最大。
- 首先最大的寬是雙指針的初始位置p_left和p_right,而矩形的高由a1、an中較小的高決定,如圖所示。
- 接着,要使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步,直到滿足遞歸終止條件(或一層遍歷結束),退出
- 退出時,回溯到上一次的狀態,解除本次對列、主對角線、副對角線的佔用
其中,如果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的矩陣,按照填充過程,填入數字:
- 從左至右,在邊界範圍內填充一行,填充完畢上邊界下移
- 從上至下,在邊界範圍內填充一行,填充完畢右邊界左移
- 從右至左,在邊界範圍內填充一行,填充完畢下邊界上移
- 從下至上,在邊界範圍內填充一行,填充完畢左邊界右移
- 數字沒有填充完就返回第一步接着填充
#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