- 《劍指offer》題解(Python版本)
- 1.使用Python實現單例模式
- 2.二維數組中的查找
- 3.替換空格
- 4.從尾到頭打印單鏈表
- 5.重建二叉樹
- 6.用兩個棧實現隊列
- 7.旋轉數組中的最小數字
- 8.斐波那契數列
- 9.二進制中1的個數
- 10.數值的整數次方
- 11.打印1到最大的n位數
- 12.O(1)時間刪除鏈表結點
- 13.調整數組順序使奇數位於偶數前面
- 14.鏈表中倒數第k個結點
- 15.反轉鏈表
- 16.合並兩個排序的鏈表
- 17.樹的子結構
- 18.二叉樹的鏡像
- 19.順時針打印矩陣
- 20.包含min函數的棧
- 21.棧的壓入彈出隊列
- 22.從上往下打印二叉樹
- 23.二叉搜索樹的后序遍歷
- 24.二叉樹中和為某一路徑值
- 25.復雜鏈表的復制
- 26.二叉搜索樹與雙向鏈表
- 27.字符串的排列
- 28.數組中出現次數超過一半的數
- 29.最小的k個數
- 30.連續子數組的最大和
- 31.從1到n整數中1出現的次數
- 32.把數組排成最小數
- 33.丑數
- 34.第一個只出現一次的字符
- 35.數組中的逆序對
- 36.兩個鏈表第一個公共節點
- 37.數字在排序數組中出現的次數
- 38.二叉樹的深度
- 39.判斷是否是平衡二叉樹
- 40.數組中只出現一次的數字
- 41.和為s的兩個數字
- 42.和為s的連續正數序列
- 43.翻轉單詞順序
- 44.左旋轉字符串
- 45.n個骰子的點數
- 45.撲克牌中的順子
- 46.圓圈中最后剩下的數字
- 47.求1+2+...n
- 48.不用加減乘除做加法
- 49.把字符串轉換成整數
- 50.樹中兩個節點的公共節點
- 補充
《劍指offer》題解(Python版本)
1.使用Python實現單例模式
方法一 使用new實現單例模式
- new(clas[,...]) 通常被用在不可變類(int,str,),是一個類實例化后首先調用的方法,后面有參數原封不動地傳給init方法,對參數定義時不用加self,需要一個實參對象作為返回值,在繼承一個不可變類,又需要重寫時才用new,重新定義后,需要return一個類對象,可以是父類,也可以是重寫后的類。
- 如果子類定義和父類同名的屬性或方法,父類的屬性或方法就會被覆蓋,此刻想再調用未被綁定的父類的方法,用父類名.方法名,或者super().需要調用的父類方法名,super()函數可以不用給出基類的名字,所以改變了類繼承關系,改變class語句里的父類即可
class SingleTon(object):
_instance = {}
def __new__(cls, *args, **kwargs):
if cls not in cls._instance:
cls._instance[cls] = super(SingleTon, cls).__new__(cls, *args, **kwargs)
# print cls._instance
return cls._instance[cls]
class MyClass(SingleTon):
class_val = 22
def __init__(self, val):
self.val = val
def obj_fun(self):
print self.val, 'obj_fun'
@staticmethod
def static_fun():
print 'staticmethod'
@classmethod
def class_fun(cls):
print cls.class_val, 'classmethod'
if __name__ == '__main__':
a = MyClass(1)
b = MyClass(2)
print(a is b) # True
print(id(a), id(b)) # 4367665424 4367665424
# 類型驗證
print type(a) # <class '__main__.MyClass'>
print type(b) # <class '__main__.MyClass'>
方法二使用裝飾器實現單例模式
from functools import wraps
def single_ton(cls):
_instance = {}
'''
裝飾器裝飾過的函數,函數名會變成single,
加上 @wraps(cls)可以避免。
希望裝飾器就可以用於任意目標函數,
可以傳入可變參數*args和關鍵字參數**kwargs。
'''
@wraps(cls)
def single(*args, **kwargs):
if cls not in _instance:
_instance[cls] = cls(*args, **kwargs)
return _instance[cls]
return single
@single_ton
class SingleTon(object):
val = 123
def __init__(self, a):
self.a = a
if __name__ == '__main__':
s = SingleTon(1)
t = SingleTon(2)
print(s is t) #True
print(s.a, t.a) #1 1
print(s.val, t.val) #123 123
方法三 使用模塊實現單例模式
可以使用模塊創建單例模式,然后在其他模塊中導入該單例
# use_module.py
class SingleTon(object):
def __init__(self, val):
self.val = val
single = SingleTon(2)
# test_module.py
from use_module import single
a = single
b = single
print(a.val, b.val)
print(a is b)
a.val = 233
print(a.val, b.val)
2.二維數組中的查找
在一個 n * m
的二維數組中,每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。
示例:
現有矩陣 matrix 如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
給定 target = 5,返回 true。
給定 target = 20,返回 false
思路:從左下角開始比較,左下角是最大行最小列,目標值比左下角值大,列號+1,目標值比左下角值小,行號-1
class Solution(object):
def findNumberIn2DArray(self, matrix, target):
"""
:type matrix: List[List[int]]
:type target: int
:rtype: bool
"""
if not matrix:
return False
rows, cols = len(matrix), len(matrix[0])
row, col = rows - 1, 0
while row >= 0 and col <= cols - 1:
if matrix[row][col] == target:
return True
#左下角值比目標值大,行號-1
elif matrix[row][col] > target:
row -= 1
#左下角值比目標值小,列號-1
else:
col += 1
return False
if __name__=="__main__":
s=Solution()
print(s.findNumberIn2DArray([
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
],5))
3.替換空格
請實現一個函數,把字符串 s 中的每個空格替換成"%20"。
示例 1:
輸入:s = "We are happy."
輸出:"We%20are%20happy."
方法1:直接使用Python字符串的內置函數replace
s.replace(' ', '20%')
方法2:插入排序
- 初始化一個 list ,記為 res ;
- 遍歷列表 s 中的每個字符 c :
當 c 為空格時:向 res 后添加字符串 "%20" ;
當 c 不為空格時:向 res 后添加字符 c ;- 將列表 res 轉化為字符串並返回。
class Solution:
def replaceSpace(self, s: str) -> str:
res = []
for c in s:
#當 c 為空格時:向 res 后添加字符串 "%20"
if c == ' ':
res.append("%20")
#當 c 不為空格時:向 res 后添加字符 c
else:
res.append(c)
return "".join(res)
if __name__=="__main__":
s=Solution()
print(s.replaceSpace("We are happy."))
#We%20are%20happy.
4.從尾到頭打印單鏈表
輸入一個鏈表的頭節點,從尾到頭反過來返回每個節點的值(用數組返回)。
示例 1:
輸入:head = [1,3,2]
輸出:[2,3,1]
在遍歷一個鏈表的時候,將值依次放入到一個list中,遍歷結束后,翻轉list輸出
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def reversePrint(self, head: ListNode):
stack=[]
while head:
stack.append(head.val)
head = head.next
if stack!=[]:
return(stack[::-1])
else:
return stack
s=Solution()
node=ListNode(1)
node.next=ListNode(2)
node.next.next=ListNode(3)
node.next.next.next=ListNode(4)
print(s.reversePrint(node))
#[4, 3, 2, 1]
遞歸+回溯:
- 遞推階段: 每次傳入 head.next ,以 head == None(即走過鏈表尾部節點)為遞歸終止條件,此時返回空列表 [] 。
- 回溯階段: 利用 Python 語言特性,遞歸回溯時每次返回 當前 list + 當前節點值 [head.val] ,即可實現節點的倒序輸出。
![]()
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def reversePrint(self, head: ListNode):
return self.reversePrint(head.next) + [head.val] if head else []
s=Solution()
node=ListNode(1)
node.next=ListNode(2)
node.next.next=ListNode(3)
node.next.next.next=ListNode(4)
print(s.reversePrint(node))
#[4, 3, 2, 1]
5.重建二叉樹
輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重復的數字。
例如,給出
前序遍歷 preorder = [3,9,20,15,7]
中序遍歷 inorder = [9,3,15,20,7]
返回如下的二叉樹:
3
/
9 20
/
15 7
遞歸:
前序遍歷+中序遍歷:
- 前序遍歷特點: 節點按照 [ 根節點 | 左子樹 | 右子樹 ] 排序,以題目示例為例:[ 3 | 9 | 20 15 7 ]
中序遍歷特點: 節點按照 [ 左子樹 | 根節點 | 右子樹 ] 排序,以題目示例為例:[ 9 | 3 | 15 20 7 ]- 前序遍歷的首個元素即為根節點
root
的值,然后在中序遍歷序列中尋找根節點root的值的位置- 從中序遍歷序列的起始位置到根結點的值的位置(不包含)為根結點左子樹的中序遍歷序列,從中序遍歷序列的根結點的值的位置(不包含)到結束位置為根結點右子樹的中序遍歷序列. 從前序遍歷序列的第二個元素開始的根結點左子樹結點數個元素的子序列為根結點左子樹的前序遍歷序列,從下一個元素開始,直到結束位置的子序列為根結點右子樹的前序遍歷序列.
- 構建根節點
root
的左子樹和右子樹: 通過調用buildTree()
方法開啟下一層遞歸,給新建的樹添加左子樹和右子樹![]()
![]()
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
if not preorder or not inorder:
return None
if len(preorder) != len(inorder):
return None
root = preorder[0]#第一個為根節點
rootNode = TreeNode(root)#創建樹
pos = inorder.index(root)#在中序中找到對應索引值
#索引中序遍歷中左右子樹位置
inorder_left = inorder[:pos]
inorder_right = inorder[pos+1:]
#索引前序遍歷中左右子樹位置
preorder_left = preorder[1:1+pos]
preorder_right = preorder[pos+1:]
node_left = self.buildTree(preorder_left , inorder_left) #遞歸左子樹
node_right= self.buildTree(preorder_right , inorder_right) #遞歸右子樹
rootNode.left = node_left#添加左子樹
rootNode.right = node_right#添加右子樹
return rootNode
#層序遍歷打印二叉樹,bfs
s=Solution()
resu=s.buildTree([ 3,9,20,15,7],[9, 3,15,20,7 ])
def levelOrder(root: TreeNode):
queue = []
res = []
if root == None:
return res
queue.append(root)
while queue:
newNode = queue.pop(0)
res.append(newNode.val)
if newNode.left != None:
queue.append(newNode.left)
if newNode.right != None:
queue.append(newNode.right)
return res
print(levelOrder(resu)) #[3, 9, 20, 15, 7]
6.用兩個棧實現隊列
用兩個棧實現一個隊列。隊列的聲明如下,請實現它的兩個函數 appendTail 和 deleteHead ,分別完成在隊列尾部插入整數和在隊列頭部刪除整數的功能。(若隊列中沒有元素,deleteHead 操作返回 -1 )
示例 :
輸入:
["CQueue","appendTail","deleteHead","deleteHead"]
[[],[3],[],[]]
輸出:[null,null,3,-1]
棧的先進后出特性:
- 棧無法實現隊列功能: 棧底元素(對應隊首元素)無法直接刪除,需要將上方所有元素出棧。
- 雙棧可實現列表倒序: 設有含三個元素的棧 A = [1,2,3]和空棧 B = []。若循環執行 A元素出棧並添加入棧 B ,直到棧 A為空,則 A = [], B = [3,2,1] ,即 棧 B 元素實現棧 A元素倒序 。
- 利用棧 B刪除隊首元素: 倒序后,B執行出棧則相當於刪除了 A 的棧底元素,即對應隊首元素。
![]()
class CQueue:
def __init__(self):
self.stack1=[]
self.stack2=[]
#在隊列尾部插入整數
def appendTail(self, value: int) -> None:
self.stack1.append(value)
#在隊列頭部刪除整數
def deleteHead(self) -> int:
if self.stack2:
return self.stack2.pop()
#將1的出棧循環放入2,則2是1的倒序,2出棧就成了1的隊列
while self.stack1:
self.stack2.append(self.stack1.pop())
return self.stack2.pop() if self.stack2 else -1
c=CQueue()
print(c.appendTail(3)) #None
print(c.deleteHead()) #3
print(c.deleteHead()) #-1
7.旋轉數組中的最小數字
把一個數組最開始的若干個元素搬到數組的末尾,我們稱之為數組的旋轉。輸入一個遞增排序的數組的一個旋轉,輸出旋轉數組的最小元素。例如,數組 [3,4,5,1,2] 為 [1,2,3,4,5] 的一個旋轉,該數組的最小值為1。
示例 1:
輸入:[3,4,5,1,2]
輸出:1
二分查找:
尋找旋轉數組的最小元素即為尋找 右排序數組 的首個元素 nums[x] ,稱 x為 旋轉點 。
![]()
- 初始化: 聲明 i, j雙指針分別指向 nums 數組左右兩端;
循環二分: 設 m = (i + j) / 2為每次二分的中點( "/" 代表向下取整除法,因此恆有 i≤m<j ),可分為以下三種情況:- 當 nums[m] > nums[j] 時: m一定在 左排序數組 中,即旋轉點 x一定在 [m + 1, j]閉區間內,因此執行 i = m + 1;
- 當 nums[m] < nums[j]: mm一定在 右排序數組 中,即旋轉點 x 一定在[i, m]閉區間內,因此執行 j = m; 當 nums[m] = nums[j]時: 無法判斷 m 在哪個排序數組中,即無法判斷旋轉點 x 在 [i, m]還是 [m + 1, j]區間中。解決方案: 執行 j = j - 1 縮小判斷范圍。 返回值: 當 i = j 時跳出二分循環,並返回 旋轉點的值 nums[i]即可
class Solution:
def minArray(self, numbers) -> int:
i, j = 0, len(numbers) - 1
while i < j:
#每次二分的中點
m = (i + j) // 2
#旋轉點 x在 [m + 1, j]閉區間內
if numbers[m] > numbers[j]:
i = m + 1
#旋轉點x在[i, m]閉區間內
elif numbers[m] < numbers[j]:
j = m
#無法判斷旋轉點 x 在 [i, m]還是 [m + 1, j]區間中
else: j -= 1
return numbers[i]
if __name__=='__main__':
s=Solution()
print(s.minArray([3,4,5,1,2]))#1
8.斐波那契數列
寫一個函數,輸入 n ,求斐波那契(Fibonacci)數列的第 n 項。斐波那契數列的定義如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契數列由 0 和 1 開始,之后的斐波那契數就是由之前的兩數相加而得出。
答案需要取模 1e9+7(1000000007),如計算初始結果為:1000000008,請返回 1
動態規划:
創建長度為n+1的數組dp,令dp[0]=0,dp[1]=1。除前兩項外,數組中某項的值為其前兩項的和,據此由前到后依次求出各項的值,最后一項即為所求
class Solution:
def fib(self, n: int) -> int:
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a % 1000000007
if __name__=='__main__':
s=Solution()
print(s.fib(3)) #2
遞歸:
return fib(n-1) + fib(n-2)
邊界:n0,return 0
n1,return1
class Solution:
def fib(self, n: int) -> int:
if n == 0:
return 0
if n == 1:
return 1
return self.fib(n-1) + self.fib(n-2)
if __name__=='__main__':
s=Solution()
print(s.fib(3)) #2
帶備忘錄的遞歸解法:
在遞歸法的基礎上,新建一個長度為 n的數組,用於在遞歸時存儲 f(0) 至 f(n)的數字值,重復遇到某數字則直接從數組取用,避免了重復的遞歸計算。
![]()
class Solution:
def fib(self, n: int) -> int:
my_dic = {0: 0, 1: 1}
def my_fib(n):
if n in my_dic:
return my_dic[n]
#運用字典,查看是否遇到重復數字
if n - 2 not in my_dic:
my_dic[n - 2] = my_fib(n - 2)
if n - 1 not in my_dic:
my_dic[n - 1] = my_fib(n - 1)
my_dic[n] = (my_dic[n - 2] + my_dic[n - 1]) % 1000000007
return my_dic[n]
return my_fib(n)
if __name__=='__main__':
s=Solution()
print(s.fib(3)) #2
9.二進制中1的個數
請實現一個函數,輸入一個整數,輸出該數二進制表示中 1 的個數。例如,把 9 表示成二進制是 1001,有 2 位是 1。因此,如果輸入 9,則該函數輸出 2。
示例 :
輸入:00000000000000000000000000001011
輸出:3
解釋:輸入的二進制串 00000000000000000000000000001011 中,共有三位為 '1'。
按位與:
二進制整數,將它-1與它本身&,會把這個整數最右邊的1變成0,直到全0為止,有多少1就可以循環多少次。對於負數,將最高位的符號取反得到補碼,通常采用和0xffffffff相與得到
class Solution:
def num_of_1(self,n):
if n<0:
n=n&0xffffffff
ret = 0
while n:
ret += 1
n = n & n - 1
return ret
if __name__=='__main__':
s=Solution()
print(s.num_of_1(11)) #3
10.數值的整數次方
實現函數double Power(double base, int exponent),求base的exponent次方。不得使用庫函數,同時不需要考慮大數問題。
示例 :
輸入: 2.00000, 10
輸出: 1024.00000
快速冪:
- 對於任何十進制正整數 nn ,設其二進制為 “\(b~m~b_3b_2b_1b\) "( \(b_i\)為二進制某位值,\(i \in [1,m]\) ),則有:
- 二進制轉十進制: \(n = 1b_1 + 2b_2 + 4b_3 + ... + 2^{m-1}b_m\)(即二進制轉十進制公式) ;
- 冪的二進制展開: \(x^n = x^{1b_1 + 2b_2 + 4b_3 + ... + 2^{m-1}b_m} = x^{1b_1}x^{2b_2}x^{4b_3}...x^{2^{m-1}b_m}\);
- 根據以上推導,可把計算 \(x^n\)x 轉化為解決以下兩個問題:
- 計算 \(x^1, x^2, x^4, ..., x^{2^{m-1}}\)的值: 循環賦值操作 \(x = x^2\)即可;
- 獲取二進制各位 \(b_1, b_2, b_3, ..., b_m\)的值: 循環執行以下操作即可。
- n&1 (與操作): 判斷 n二進制最右一位是否為1
- n>>1(移位操作): n右移一位(可理解為刪除最后一位)。
- 因此,應用以上操作,可在循環中依次計算 \(x^{2^{0}b_1}, x^{2^{1}b_2}, ..., x^{2^{m-1}b_m}\)的值,並將所有 \(x^{2^{i-1}b_i}\)累計相乘即可。
- 當 \(b_i = 0\)時:\(x^{2^{i-1}b_i} = 1\);
- 當 \(b_i = 1\)時:\(x^{2^{i-1}b_i} = x^{2^{i-1}}\);
![]()
class Solution:
def myPow(self, x: float, n: int) -> float:
if x == 0: return 0
res = 1
#當 n < 0時:把問題轉化至n≥0 的范圍內
if n < 0:
x, n = 1 / x, -n
while n:
#判斷二進制形式最后一位是否為1,為1與為1,為0與為0,==n%2
if n & 1:
res *= x
x *= x
#二進制形式右移一位,=n//2
n >>= 1
return res
if __name__=='__main__':
s=Solution()
print(s.myPow(2,3)) #8
遞歸+二分推導:
二分推導: \(x^n = x^{n/2} \times x^{n/2} = (x^2)^{n/2}\) ,令 n/2 為整數,則需要分為奇偶兩種情況(設向下取整除法符號為 "//" ):
- 當 n為偶數: \(x^n = (x^2)^{n//2}\);
- 當 n為奇數: \(x^n = x(x^2)^{n//2}\)即會多出一項 x ;
class Solution:
def myPow(self, x: float, n: int) -> float:
if n == 0:
return 1
if n < 0:
return 1 / self.myPow(x, -n)
# 如果是奇數
if n & 1:
return x * self.myPow(x, n - 1)
# 如果是偶數
return self.myPow(x * x, n // 2)
if __name__=='__main__':
s=Solution()
print(s.myPow(2,3)) #8
11.打印1到最大的n位數
輸入數字 n,按順序打印出從 1 到最大的 n 位十進制數。比如輸入 3,則打印出 1、2、3 一直到最大的 3 位數 999。
示例 :
輸入: n = 1
輸出: [1,2,3,4,5,6,7,8,9]
先使用
range()
方法生成可迭代對象,再使用list()
方法轉化為列表並返回即可
class Solution:
def printNumbers(self, n: int) :
return list(range(1, 10 ** n))
if __name__=='__main__':
s=Solution()
print(s.printNumbers(1)) #[1, 2, 3, 4, 5, 6, 7, 8, 9]
大數越界情況下:
表示大數的變量類型:
無論是 short / int / long ... 任意變量類型,數字的取值范圍都是有限的。因此,大數的表示應用字符串 String 類型。生成數字的字符串集:
使用 int 類型時,每輪可通過 +1生成下個數字,而此方法無法應用至 String 類型。並且, String 類型的數字的進位操作效率較低,例如 "9999" 至 "10000" 需要從個位到千位循環判斷,進位 4 次。觀察可知,生成的列表實際上是 n位 00 - 99 的 全排列 ,因此可避開進位操作,通過遞歸生成數字的 String 列表。
- 遞歸生成全排列:
基於分治算法的思想,先固定高位,向低位遞歸,當個位已被固定時,添加數字的字符串。例如當 n = 2時(數字范圍 1 - 99),固定十位為 00 - 99 ,按順序依次開啟遞歸,固定個位 00 - 99 ,終止遞歸並添加數字字符串。- 刪除高位多余0:高位為0時不用遍歷來設置高位。
class Solution:
def printNumbers(self, n: int) :
def dfs(x,begin0):
if x == n: # 終止條件:已固定完所有位
s=''.join(num) # 拼接 num
if s:
res.append(int(s)) # 將拼接 的num轉成int類型 並添加至 res 尾部
return
for i in range(10): # 遍歷 0 - 9
if begin0 and i!=0: #刪除高位多余的0
begin0=False
if not begin0:
num[x] = str(i) # 固定第 x 位為 i
dfs(x + 1,begin0) # 開啟固定第 x + 1 位
begin0 = True
num = [''] * n # 起始數字定義為 n 個 空字符組成的字符列表
res = [] # 數字字符串列表
dfs(0,begin0) # 開啟全排列遞歸
return res # 拼接所有數字字符串,使用逗號隔開,並返回
if __name__=='__main__':
s=Solution()
print(s.printNumbers(1)) #[1, 2, 3, 4, 5, 6, 7, 8, 9]
12.O(1)時間刪除鏈表結點
給定單向鏈表的頭指針和一個要刪除的節點的值,定義一個函數刪除該節點。
返回刪除后的鏈表的頭節點。
示例 :
輸入: head = [4,5,1,9], val = 5
輸出: [4,1,9]
解釋: 給定你鏈表中值為 5 的第二個節點,那么在調用了你的函數之后,該鏈表應變為 4 -> 1 -> 9.
雙指針:
- 特例處理: 當應刪除頭節點 head 時,直接返回 head.next 即可。
- 初始化: pre = head , cur = head.next 。
- 定位節點: 當 cur 為空 或 cur 節點值等於 val 時跳出。
- 保存當前節點索引,即 pre = cur 。
- 遍歷下一節點,即 cur = cur.next 。
- 刪除節點: 若 cur 指向某節點,則執行 pre.next = cur.next 。(若 cur 指向 nullnull ,代表鏈表中不包含值為 val 的節點。
- 返回值: 返回鏈表頭部節點 head 即可
![]()
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def deleteNode(self, head: ListNode, val: int) -> ListNode:
#當應刪除頭節點 head 時,直接返回 head.next 即可
if head.val==val:
return head.next
pre, cur = head, head.next
#當 cur 為空 或 cur 節點值等於 val 時跳出
while cur and cur.val != val:
pre, cur = cur, cur.next #1. 保存當前節點索引,遍歷下一節點
#若 cur 指向某節點,刪除結點
if cur.val==val:
pre.next = cur.next
return head
s=Solution()
node=ListNode(1)
node.next=ListNode(2)
node.next.next=ListNode(3)
node.next.next.next=ListNode(4)
resu=s.deleteNode(node,3)
#從頭到尾打印鏈表
def reversePrint(head: ListNode):
stack = []
while head:
stack.append(head.val)
head = head.next
return stack
print(reversePrint(resu))#[1, 2, 4]
13.調整數組順序使奇數位於偶數前面
輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得所有奇數位於數組的前半部分,所有偶數位於數組的后半部分。
示例:
輸入:nums = [1,2,3,4]
輸出:[1,3,2,4]
注:[3,1,2,4] 也是正確的答案之一
雙指針:
- 初始化: i, j雙指針,分別指向數組 nums左右兩端;
- 循環交換: 當 i = j時跳出;
- 指針 i遇到奇數則執行 i = i + 1跳過,直到找到偶數;
- 指針 j遇到偶數則執行 j = j - 1 跳過,直到找到奇數;
- 交換 nums[i] 和 nums[j]值;
- 返回值: 返回已修改的 numsnums 數組
![]()
class Solution:
def exchange(self, nums):
i, j = 0, len(nums) - 1
while i < j:
#指針 i遇到奇數則執行 i = i + 1跳過,直到找到偶數
while i < j and nums[i] & 1 == 1: i += 1
#指針 j遇到偶數則執行 j = j - 1 跳過,直到找到奇數
while i < j and nums[j] & 1 == 0: j -= 1
nums[i], nums[j] = nums[j], nums[i]
return nums
if __name__=='__main__':
s=Solution()
print(s.exchange([1,2,3,4])) #[1, 3, 2, 4]
sorted()
排序:偶數%2=0,奇數%2=1,1-偶數%2=1排在后頭,奇數%2=0排在前頭,可以實現奇數在前,偶數在后
class Solution:
def exchange(self, nums):
return sorted(nums,key=lambda x:1-x%2)
if __name__=='__main__':
s=Solution()
print(s.exchange([1,2,3,4])) #[1, 3, 2, 4]
14.鏈表中倒數第k個結點
輸入一個鏈表,輸出該鏈表中倒數第k個節點。為了符合大多數人的習慣,本題從1開始計數,即鏈表的尾節點是倒數第1個節點。例如,一個鏈表有6個節點,從頭節點開始,它們的值依次是1、2、3、4、5、6。這個鏈表的倒數第3個節點是值為4的節點。
示例:
給定一個鏈表: 1->2->3->4->5, 和 k = 2.
返回鏈表 4->5.
快慢指針:
- 初始化: 前指針 former 、后指針 latter ,雙指針都指向頭節點 head 。
- 構建雙指針距離: 前指針 former 先向前走 k 步(結束后,雙指針 former 和 latter 間相距 k 步)。
- 雙指針共同移動: 循環中,雙指針 former 和 latter 每輪都向前走一步,直至 former 走過鏈表 尾節點 時跳出(跳出后, latter 與尾節點距離為 k-1,即 latter 指向倒數第 k個節點)。
- 返回值: 返回 latter 即可
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
former, latter = head, head
#前指針 former 先向前走 k 步
for _ in range(k):
former = former.next
#循環中,雙指針 former 和 latter 每輪都向前走一步,到former為空時
while former:
former, latter = former.next, latter.next
#latter指向倒數第 k 個節點
return latter
#生成鏈表
s=Solution()
node=ListNode(1)
node.next=ListNode(2)
node.next.next=ListNode(3)
node.next.next.next=ListNode(4)
node.next.next.next.next=ListNode(5)
resu=s.getKthFromEnd(node,2)
#從頭到尾打印鏈表
def reversePrint(head: ListNode):
stack = []
while head:
stack.append(head.val)
head = head.next
return stack
print(reversePrint(resu))#[4, 5]
遍歷+指針:
- 先遍歷統計鏈表長度,記為 n;
- 設置一個指針走 (n-k)步,即可找到鏈表倒數第 k個節點
![]()
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def getKthFromEnd(self, head: ListNode, k: int) -> ListNode:
count = 0
node = head
# 遍歷鏈表長度count
while node:
count += 1
node = node.next
former = head
n = count - k
# 設置一個指針走count-k步驟
while n:
former = former.next
n -= 1
return former
#生成鏈表
s=Solution()
node=ListNode(1)
node.next=ListNode(2)
node.next.next=ListNode(3)
node.next.next.next=ListNode(4)
node.next.next.next.next=ListNode(5)
resu=s.getKthFromEnd(node,2)
#從頭到尾打印鏈表
def reversePrint(head: ListNode):
stack = []
while head:
stack.append(head.val)
head = head.next
return stack
print(reversePrint(resu))#[4, 5]
15.反轉鏈表
定義一個函數,輸入一個鏈表的頭節點,反轉該鏈表並輸出反轉后鏈表的頭節點。
示例:
輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
雙指針:
- 一個指針pre指向null做前一節點,另一個指針cur指向鏈表head表頭,做當前節點
- 循環遍歷鏈表,當前節點為空時結束循環
- 當前節點cur的下個節點指向前一個節點pre
- 指針pre用作新生成的一個鏈表當前節點,另一個指針cur用於源鏈表遍歷
- 更新新鏈表,返回新表表頭pre
![]()
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
# 雙指針,一個指針用作新生成的一個鏈表當前節點,另一個指針用於源鏈表遍歷
def reverseList(self, head: ListNode) -> ListNode:
pre=None
cur=head
#當前節點為空時結束循環
while cur:
#單值賦值需要臨時變量,多值賦值不用
#cur.next, pre, cur = pre, cur, cur.next
# 這個臨時節點就相當於一個副本
temp=cur.next
#當前節點的下個節點指向前一個節點
cur.next=pre
#保存當前節點
pre=cur
#遍歷下個節點
cur=temp
#當前節點為空,前一個節點是遍歷的最后一個節點
return pre
#生成鏈表
s=Solution()
node=ListNode(1)
node.next=ListNode(2)
node.next.next=ListNode(3)
node.next.next.next=ListNode(4)
node.next.next.next.next=ListNode(5)
resu=s.reverseList(node)
#從頭到尾打印鏈表
def reversePrint(head: ListNode):
stack = []
while head:
stack.append(head.val)
head = head.next
return stack
print(reversePrint(resu))#[5, 4, 3, 2, 1]
16.合並兩個排序的鏈表
輸入兩個遞增排序的鏈表,合並這兩個鏈表並使新鏈表中的節點仍然是遞增排序的。
示例1:
輸入:1->2->4, 1->3->4
輸出:1->1->2->3->4->4
雙指針:
根據題目描述, 鏈表 \(l_1\), \(l_2\)是 遞增 的,因此容易想到使用雙指針\(l_1\), \(l_2\)遍歷兩鏈表,根據 \(l_1\).vall 和 \(l_2.\)vall 的大小關系確定節點添加順序,兩節點指針交替前進,直至遍歷完畢。
引入偽頭節點: 由於初始狀態合並鏈表中無節點,因此循環第一輪時無法將節點添加到合並鏈表中。解決方案:初始化一個輔助節點 dump作為合並鏈表的偽頭節點,將各節點添加至 dump后。
![]()
- 初始化: 偽頭節點 dump,節點 cur指向 dump 。
- 循環合並:
- 當\(l_1\)或 \(l_2\)為空時跳出;
- 當 \(l_1.vall <l_2.vall\)時: cur 的后繼節點指定為 \(l_1\),並 \(l_1\)向前走一步;
- 當 \(l_1.val \geq l_2.vall\) 時: cur的后繼節點指定為 \(l_2\),並 \(l_2\) 向前走一步 ;
- 節點 cur向前走一步,即 cur = cur.next 。
- 合並剩余尾部: 跳出時有兩種情況,即 \(l_1\)為空 或 \(l_2\)為空。
- 若 \(l_1 \ne nulll\) : 將 \(l_1\)添加至節點 cur 之后;
- 否則: 將 \(l_2\)添加至節點 cur 之后。
- 返回值: 合並鏈表在偽頭節點 dump之后,因此返回 dump.next 即可
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
#合並兩個排序列表
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
cur=dump=ListNode(0)
while l1 and l2:
if l1.val<l2.val:
#下一節點指向較小的節點
cur.next=l1
l1=l1.next
else:
cur.next=l2
l2=l2.next
#節點 cur向前走一步
cur=cur.next
#若 l1!=None: 將 l1添加至節點 cur 之后
cur.next=l1 if l1 else l2
return dump.next
#生成鏈表
s=Solution()
node1=ListNode(1)
node1.next=ListNode(2)
node1.next.next=ListNode(4)
node2=ListNode(1)
node2.next=ListNode(3)
node2.next.next=ListNode(4)
resu=s.mergeTwoLists(node1,node2)
#從頭到尾打印鏈表
def reversePrint(head: ListNode):
stack = []
while head:
stack.append(head.val)
head = head.next
return stack
print(reversePrint(resu))#[1, 1, 2, 3, 4, 4]
17.樹的子結構
輸入兩棵二叉樹A和B,判斷B是不是A的子結構。(約定空樹不是任意一個樹的子結構)
B是A的子結構, 即 A中有出現和B相同的結構和節點值。
例如:
給定的樹 A:
3
/ \
4 5
/
1 2
給定的樹 B:
4
/
1
返回 true,因為 B 與 A 的一個子樹擁有相同的結構和節點值。
先序遍歷+遞歸:
- 序遍歷樹 A 中的每個節點\(n_A\) ;(對應函數 isSubStructure(A, B))
- 判斷樹 A中 以\(n_A\) 為根節點的子樹 是否包含樹 B 。(對應函數 recur(A, B))
- recur(A, B) 函數:
- 終止條件:
當節點 B為空:說明樹 B 已匹配完成(越過葉子節點),因此返回 true ;
當節點 A為空:說明已經越過樹 A 葉子節點,即匹配失敗,返回 false ;
當節點 A和 B 的值不同:說明匹配失敗,返回 false ;- 返回值:
判斷 A 和 B的左子節點是否相等,即 recur(A.left, B.left) ;
判斷 A和 B 的右子節點是否相等,即 recur(A.right, B.right) ;- isSubStructure(A, B) 函數:
- 特例處理: 當 樹 A為空 或 樹 B 為空 時,直接返回 false;
- 返回值: 若樹 B是樹 A 的子結構,則必滿足以下三種情況之一,因此用或 || 連接;
以 節點 A為根節點的子樹 包含樹 B ,對應 recur(A, B);
樹 B是 樹 A左子樹 的子結構,對應 isSubStructure(A.left, B);
樹 B是 樹 A右子樹 的子結構,對應 isSubStructure(A.right, B);![]()
#定義樹
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
#判斷B是否是A子樹
class Solution:
def isSubStructure(self, A: TreeNode, B: TreeNode) -> bool:
def recur(A, B):
#B全遞歸完成空,此時B前面的值都與A相等,是子樹
if not B:
return True
#A的值先遞歸完了,B的值還剩,A中無完全與B相等
if not A or A.val != B.val:
return False
#A,B值相等時遞歸左右子樹
return recur(A.left, B.left) and recur(A.right, B.right)
#A、B都存在且滿足以節點A為根節點的子樹包含樹 B,樹 B是 樹 A左/右子樹 的子結構條件之一
return bool(A and B) and (recur(A, B) or self.isSubStructure(A.left, B) or self.isSubStructure(A.right, B))
#生成二叉樹
s=Solution()
node1=TreeNode(3)
node1.left=TreeNode(4)
node1.right=TreeNode(5)
node1.left.left=TreeNode(1)
node1.left.right=TreeNode(2)
node2=TreeNode(4)
node2.left=TreeNode(1)
print(s.isSubStructure(node1,node2)) #True
18.二叉樹的鏡像
請完成一個函數,輸入一個二叉樹,該函數輸出它的鏡像。
例如輸入:
4
/
2 7
/ \ /
1 3 6 9
鏡像輸出:
4
/
7 2
/ \ /
9 6 3 1
遞歸:
- 終止條件: 當節點 root為空時(即越過葉節點),則返回 null;
- 開啟遞歸 右子節點 mirrorTree(root.right),並將返回值作為 root的 左子節點 。
開啟遞歸 左子節點 mirrorTree(tmp) ,並將返回值作為 root的 右子節點- 返回值: 返回當前節點 rootroo**t
#定義樹
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
#二叉樹鏡像
class Solution:
def mirrorTree(self, root: TreeNode) -> TreeNode:
if not root: return
#遞歸並交換左右節點
root.left, root.right = self.mirrorTree(root.right), self.mirrorTree(root.left)
return root
#生成二叉樹
s=Solution()
node1=TreeNode(4)
node1.left=TreeNode(2)
node1.right=TreeNode(7)
node1.left.left=TreeNode(1)
node1.left.right=TreeNode(3)
node1.right.left=TreeNode(6)
node1.right.right=TreeNode(9)
resu=s.mirrorTree(node1)
#層序遍歷打印二叉樹,bfs
def levelOrder(root: TreeNode):
queue = []
res = []
if root == None:
return res
queue.append(root)
while queue:
newNode = queue.pop(0)
res.append(newNode.val)
if newNode.left != None:
queue.append(newNode.left)
if newNode.right != None:
queue.append(newNode.right)
return res
print(levelOrder(resu))#[4, 7, 2, 9, 6, 3, 1]
19.順時針打印矩陣
輸入一個矩陣,按照從外向里以順時針的順序依次打印出每一個數字。
示例 1:
輸入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
輸出:[1,2,3,6,9,8,7,4,5]
示例 2:
設定邊界:
空值處理: 當 matrix 為空時,直接返回空列表 [] 即可。
初始化: 矩陣 左、右、上、下 四個邊界 l , r , t , b ,用於打印的結果列表 res 。
循環打印: “從左向右、從上向下、從右向左、從下向上” 四個方向循環,每個方向打印中做以下三件事 (各方向的具體信息見下表) ;
根據邊界打印,即將元素按順序添加至列表 res 尾部;
邊界向內收縮 11 (代表已被打印);
判斷是否打印完畢(邊界是否相遇),若打印完畢則跳出。
返回值: 返回 res 即可。打印方向 、1. 根據邊界打印 2. 邊界向內收縮 3. 是否打印完畢
從左向右 左邊界l ,右邊界 r 上邊界 t 加 1 是否 t > b
從上向下 上邊界 t ,下邊界b 右邊界 r 減 1 是否 l > r
從右向左 右邊界 r ,左邊界l 下邊界 b 減 1 是否 t > b
從下向上 下邊界 b ,上邊界t 左邊界 l 加 1 是否 l > r![]()
class Solution:
def spiralOrder(self, matrix:[[int]]) -> [int]:
if not matrix: return []
l, r, t, b, res = 0, len(matrix[0]) - 1, 0, len(matrix) - 1, []
while True:
#從左到右到達上右邊界,向下+1
for i in range(l, r + 1):
res.append(matrix[t][i]) # left to right
t += 1
if t > b: break
# 從上往下到達右下邊界,向右-1
for i in range(t, b + 1):
res.append(matrix[i][r]) # top to bottom
r -= 1
if l > r: break
# 從右往左到達左下邊界(每次向右-1),向下-1
for i in range(r, l - 1, -1):
res.append(matrix[b][i]) # right to left
b -= 1
if t > b: break
# 從下到上到達左上邊界(每次向下-1),向上-1
for i in range(b, t - 1, -1):
res.append(matrix[i][l]) # bottom to top
l += 1
if l > r: break
return res
#測試用例
if __name__=='__main__':
s=Solution()
print(s.spiralOrder([[1,2,3],[4,5,6],[7,8,9]]))#[1,2,3,6,9,8,7,4,5]
20.包含min函數的棧
定義棧的數據結構,請在該類型中實現一個能夠得到棧的最小元素的 min 函數在該棧中,調用 min、push 及 pop 的時間復雜度都是 O(1)。
示例:
MinStack minStack = new MinStack();
minStack.push(-2);
minStack.push(0);
minStack.push(-3);
minStack.min(); --> 返回 -3.
minStack.pop();
minStack.top(); --> 返回 0.
minStack.min(); --> 返回 -2.
輔助棧:
一個常規列表實現棧的操作外,再開一個輔助棧用於保存當前的最小信息
- 入棧操作:當輔助棧為空或者輔助棧頂元素小於新元素時,輔助棧入棧;否則無視
- 出棧操作:當常規棧中待出棧的元素等於輔助棧頂元素時,輔助棧出棧一個元素,代表當前的最小值出隊或者次數減1
- 棧頂操作:僅需從常規棧頂取元素即可
- 最小值操作:因為輔助棧中維護的都是當前狀態下的最小值,所以從輔助棧頂取元素即可
#包含min函數的棧
class MinStack():
def __init__(self):
self.stack=[]
self.mini=[]
#入棧
def push(self, x: int) -> None:
self.stack.append(x)
if self.mini and self.mini[-1]<x:
self.mini.append(self.mini[-1])
else:
self.mini.append(x)
#出棧
def pop(self) -> None:
if not self.stack:
return
x = self.stack.pop()
if self.mini and self.mini[-1] == x:
self.mini.pop()
#棧頂
def top(self) -> int:
if self.stack:
return self.stack[-1]
return None
#最小值
def min(self) -> int:
if self.mini:
return self.mini[-1]
return None
if __name__=='__main__':
m=MinStack()
m.push(-2)
m.push(0)
m.push(-3)
print(m.min())#-3
m.pop()
print(m.top())#0
print(m.min())#-2
21.棧的壓入彈出隊列
輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如,序列 {1,2,3,4,5} 是某棧的壓棧序列,序列 {4,5,3,2,1} 是該壓棧序列對應的一個彈出序列,但 {4,3,5,1,2} 就不可能是該壓棧序列的彈出序列。
示例 1:
輸入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
輸出:true
解釋:我們可以按以下順序執行:
push(1), push(2), push(3), push(4), pop() -> 4,
push(5), pop() -> 5, pop() -> 3, pop() -> 2, pop() -> 1
用一個輔助棧 stack ,模擬 壓入 / 彈出操作的排列
- 初始化: 輔助棧 stack ,彈出序列的索引 i ;
- 遍歷壓棧序列: 各元素記為 num ;元素 num 入棧;
- 循環出棧:若 stack 的棧頂元素 == 彈出序列元素 popped[i],則執行出棧與 i++;
- stack為空,則此彈出序列合法
#棧的壓入彈出序列
class Solution():
def validateStackSequences(self, pushed, popped) -> bool:
#輔助棧stack模擬出棧
stack, i = [], 0
for num in pushed:
stack.append(num) # num 入棧
while stack and stack[-1] == popped[i]: # 循環判斷與出棧
stack.pop()
i += 1
return not stack
if __name__=='__main__':
s=Solution()
print(s.validateStackSequences([1,2,3,4,5],[4,5,3,2,1]))#True
print(s.validateStackSequences([1, 2, 3, 4, 5], [4, 5, 3, 1, 2]))#False
22.從上往下打印二叉樹
從上到下打印出二叉樹的每個節點,同一層的節點按照從左到右的順序打印。
示例:
給定二叉樹: [3,9,20,null,null,15,7],
3
/
9 20
/
15 7
返回:
[3,9,20,15,7]
層序遍歷 BFS+遞歸
- 特例處理: 當樹的根節點為空,則直接返回空列表 [] ;
- 初始化: 打印結果列表 res = [] ,包含根節點的隊列 queue = [root] ;
- queue 為空時跳出;
- 出隊: 隊首元素出隊,記為 newNode;
- 打印: 將 newNode.val 添加至列表 res 尾部;
- 添加子節點: 若 newNode 的左(右)子節點不為空,則將左(右)子節點加入隊列 queue ;
- 返回值: 返回打印結果列表 res 即可。
#定義樹
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
#層序遍歷打印二叉樹,bfs
class Solution():
#用一個輔助棧queue前序遍歷二叉樹,通過newNode將其隊首出棧,實現層序遍歷
#將val的值放入結果棧res中
def levelOrder(self,root: TreeNode):
queue = []
res = []
if root == None:
return res
queue.append(root)
while queue:
newNode = queue.pop(0)
res.append(newNode.val)
if newNode.left != None:
queue.append(newNode.left)
if newNode.right != None:
queue.append(newNode.right)
return res
#生成二叉樹
s=Solution()
node1=TreeNode(3)
node1.left=TreeNode(9)
node1.right=TreeNode(20)
node1.right.left=TreeNode(15)
node1.right.right=TreeNode(7)
print(s.levelOrder(node1))#[3, 9, 20, 15, 7]
23.二叉搜索樹的后序遍歷
輸入一個整數數組,判斷該數組是不是某二叉搜索樹的后序遍歷結果。如果是則返回 true,否則返回 false。假設輸入的數組的任意兩個數字都互不相同。
參考以下這顆二叉搜索樹:
5
/ \
2 6
/
1 3
示例 :
輸入: [1,6,3,2,5] 輸出: false
輸入: [1,3,2,6,5] 輸出: true
遞歸+分治:
后序遍歷定義: [ 左子樹 | 右子樹 | 根節點 ] ,即遍歷順序為 “左、右、根” 。
二叉搜索樹定義: 左子樹中所有節點的值 << 根節點的值;右子樹中所有節點的值 >> 根節點的值;其左、右子樹也分別為二叉搜索樹![]()
- 終止條件: 當 i≥j ,說明此子樹節點數量 ≤1 ,無需判別正確性,因此直接返回 true ;
- 遞推工作:
- 划分左右子樹: 遍歷后序遍歷的 \([i, j]\) 區間元素,尋找 第一個大於根節點 的節點,索引記為 m。此時,可划分出左子樹區間 \([i,m-1]\) 、右子樹區間 \([m, j - 1]\) 、根節點索引 j。
- 判斷是否為二叉搜索樹:
左子樹區間 內\([i,m-1]\)的所有節點都應 <postorder[j]。而第 1.划分左右子樹 步驟已經保證左子樹區間的正確性,因此只需要判斷右子樹區間即可。
右子樹區間 \([m, j - 1]\)內的所有節點都應 > postorder[j]。實現方式為遍歷,當遇到 \leq \(postorder[j]≤postorder[j]\) 的節點則跳出;則可通過 p = j判斷是否為二叉搜索樹。- 返回值: 所有子樹都需正確才可判定正確,因此使用 與邏輯符 and 連接。
p = j : 判斷 此樹 是否正確。
recur(i, m - 1): 判斷 此樹的左子樹 是否正確。
recur(m, j - 1): 判斷 此樹的右子樹 是否正確
#定義樹
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
#二叉搜索樹后序遍歷
class Solution():
def verifyPostorder(self, postorder) -> bool:
def recur(i, j):
if i >= j:
return True
p = i
#最右節點是根節點,通過遍歷,查找出第一個比根大的節點,記為m
while postorder[p] < postorder[j]:
p += 1
m = p
#繼續向后遍歷,看右子樹區間是不是都比跟節點大
#右子樹區間開頭第一個比根大,左子樹區間都比根小
while postorder[p] > postorder[j]:
p += 1
#此樹,此樹左子樹,右子樹都正確才正確
return p == j and recur(i, m - 1) and recur(m, j - 1)
return recur(0, len(postorder) - 1)
s=Solution()
print(s.verifyPostorder([1,6,3,2,5]))#False
print(s.verifyPostorder([1,3,2,6,5]))#True
24.二叉樹中和為某一路徑值
輸入一棵二叉樹和一個整數,打印出二叉樹中節點值的和為輸入整數的所有路徑。從樹的根節點開始往下一直到葉節點所經過的節點形成一條路徑。
示例:
給定如下二叉樹,以及目標和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ / \
7 2 5 1
返回:
[
[5,4,11,2],
[5,8,4,5]
]
先序遍歷+遞歸回溯:
先序遍歷: 按照 “根、左、右” 的順序,遍歷樹的所有節點。
路徑記錄: 在先序遍歷中,記錄從根節點到當前節點的路徑。當路徑為 ① 根節點到葉節點形成的路徑 且 ② 各節點值的和等於目標值 sum 時,將此路徑加入結果列表![]()
pathSum(root, sum) 函數:
- 初始化: 結果列表 res ,路徑列表 path 。
- 返回值: 返回 res 即可。
recur(root, tar) 函數:(多個遞歸函數,主要避免遞歸時把res,path重置為[])
- 遞推參數: 當前節點 root ,當前目標值 tar 。
- 終止條件: 若節點 root 為空,則直接返回。
- 遞推工作:
- 路徑更新: 將當前節點值 root.val 加入路徑 path ;
- 目標值更新: tar = tar - root.val(即目標值 tar 從 sum 減至 00 );
- 路徑記錄: 當 ① root 為葉節點 且 ② 路徑和等於目標值 ,則將此路徑 path 加入 res 。
- 先序遍歷: 遞歸左 / 右子節點。
- 路徑恢復: 向上回溯前,需要將當前節點從路徑 path 中刪除,即執行 path.pop()
#定義樹
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
class Solution:
def pathSum(self, root: TreeNode, sum: int) :
#記錄結果、路徑
res, path = [], []
def recur(root, tar):
if not root: return
#記錄路徑
path.append(root.val)
tar -= root.val
#路徑達到目標值,保存到結果
if tar == 0 and not root.left and not root.right:
res.append(list(path))
#先序遍歷樹
recur(root.left, tar)
recur(root.right, tar)
#在路徑中刪除當前節點,向上回溯
path.pop()
recur(root, sum)
return res
#生成二叉樹
node1=TreeNode(5)
node1.left=TreeNode(4)
node1.right=TreeNode(8)
node1.left.left=TreeNode(11)
node1.right.left=TreeNode(13)
node1.right.right=TreeNode(4)
node1.left.left.left=TreeNode(7)
node1.left.left.right=TreeNode(2)
node1.right.right.left=TreeNode(5)
node1.right.right.right=TreeNode(1)
s=Solution()
print(s.pathSum(node1,22))#[[5, 4, 11, 2], [5, 8, 4, 5]]
25.復雜鏈表的復制
請實現 copyRandomList 函數,復制一個復雜鏈表。在復雜鏈表中,每個節點除了有一個 next 指針指向下一個節點,還有一個 random 指針指向鏈表中的任意節點或者 null。
示例 :
輸入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
輸出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
DFS深度查詢+hashmap
- 從頭結點 head 開始拷貝;
- 由於一個結點可能被多個指針指到,因此如果該結點已被拷貝,則不需要重復拷貝;
- 如果還沒拷貝該結點,則創建一個新的結點進行拷貝,並將拷貝過的結點保存在哈希表中;
- 使用遞歸拷貝所有的 next 結點,再遞歸拷貝所有的 random 結點
#定義鏈表
class Node:
def __init__(self, x, next=None, random=None):
self.val = x
self.next = next
self.random = random
class Solution:
def copyRandomList(self, head) :
def dfs(head):
if not head: return None
if head in visited:
return visited[head]
# 創建新結點
copy = Node(head.val, None, None)
#新節點記錄在hash表中
visited[head] = copy
#遞歸遍歷鏈表
copy.next = dfs(head.next)
copy.random = dfs(head.random)
return copy
visited = {}
return dfs(head)
#生成復雜鏈表
node1=Node(7)
node1.next=Node(13)
node1.random=Node(None)
node1.next.next=Node(11)
node1.next.random=Node(7)
node1.next.next.next=Node(10)
node1.next.next.random=Node(1)
node1.next.next.next.next=Node(1)
node1.next.next.next.random=Node(11)
node1.next.next.next.next.random=Node(7)
s=Solution()
reu=s.copyRandomList(node1)
#打印復雜鏈表
stack1=[]
def Clone(head):
stack2 = []
if not head:
return
newNode = Node(head.val)
stack2.append(newNode.val)
stack1.append(stack2)
newNode.random = Clone(head.random)
newNode.next = Clone(head.next)
return stack1
print(Clone(reu))#[[7], [None], [13], [7], [11], [1], [10], [11], [1], [7]]
26.二叉搜索樹與雙向鏈表
輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的循環雙向鏈表。要求不能創建任何新的節點,只能調整樹中節點指針的指向。
以下面的二叉搜索樹為例:

將這個二叉搜索樹轉化為雙向循環鏈表。鏈表中的每個節點都有一個前驅和后繼指針。對於雙向循環鏈表,第一個節點的前驅是最后一個節點,最后一個節點的后繼是第一個節點。
下圖展示了上面的二叉搜索樹轉化成的鏈表。“head” 表示指向鏈表中有最小元素的節點。
中序遍歷+雙指針
- 排序鏈表: 節點應從小到大排序,因此應使用 中序遍歷 “從小到大”訪問樹的節點;
- 雙向鏈表: 在構建相鄰節點(設前驅節點 pre ,當前節點 cur )關系時,不僅應 pre.right = cur ,也應 cur.left = pre 。
- 循環鏈表: 設鏈表頭節點 head和尾節點 tail ,則應構建 head.left = tail 和 tail.right = head
![]()
#定義樹:
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def treeToDoublyList(self, root: 'Node') -> 'Node':
def dfs(cur):
if not cur:
return
dfs(cur.left)# 遞歸左子樹
if self.pre:# 修改節點引用
self.pre.right,cur.left=cur,self.pre
else:# prepre 為空時,正在訪問頭節點
self.head=cur
self.pre=cur # 保存 當前節點cur
dfs(cur.right)# 遞歸右子樹
if not root:
return
self.pre=None
dfs(root)
#self.head.left,self.pre.right=self.pre,self.head#循環鏈表頭尾
return self.head
#生成樹
node1=TreeNode(5)
node1.left=TreeNode(2)
node1.right=TreeNode(6)
node1.left.left=TreeNode(1)
node1.left.right=TreeNode(3)
s=Solution()
resu=s.treeToDoublyList(node1)
#從頭到尾打印鏈表
def reversePrint(head):
stack = []
while head:
stack.append(head.val)
head = head.right
return stack
print(reversePrint(resu))#[1, 2, 3, 5, 6]
27.字符串的排列
輸入一個字符串,打印出該字符串中字符的所有排列。
你可以以任意順序返回這個字符串數組,但里面不能有重復元素。
示例:
輸入:s = "abc"
輸出:["abc","acb","bac","bca","cab","cba"]
遞歸剪枝(重復continue)+回溯(字符交換)
![]()
- 終止條件: 當 x = len(c) - 1x=len(c)−1 時,代表所有位已固定(最后一位只有 11 種情況),則將當前組合 c 轉化為字符串並加入 res,並返回;
- 遞推參數: 當前固定位 x ;
- 遞推工作: 初始化一個 Set ,用於排除重復的字符;將第 x位字符與 \(i \in [x, len(c)]\) 字符分別交換,並進入下層遞歸;
- 剪枝: 若 c[i]在 Set 中,代表其是重復字符,因此“剪枝”;
將 c[i]加入 Set ,以便之后遇到重復字符時剪枝;- 固定字符: 將字符 c[i] 和 c[x]交換,即固定 c[i]為當前位字符;
- 開啟下層遞歸: 調用 dfs(x + 1) ,即開始固定第 x + 1個字符;
- 還原交換: 將字符 c[i]和 c[x]交換(還原之前的交換)
class Solution:
def permutation(self, s: str) :
c, res = list(s), []
def dfs(x):
if x == len(c) - 1:
res.append(''.join(c)) # 添加排列方案
return
dic = set()
for i in range(x,len(c)):
if c[i] in dic:
continue # 重復,因此剪枝
dic.add(c[i])
c[i], c[x] = c[x], c[i] #交換,將 c[i] 固定在第 x 位
dfs(x + 1) # 開啟固定第 x + 1 位字符
c[i], c[x] = c[x], c[i] # 恢復交換
dfs(0)
return res
if __name__=='__main__':
s=Solution()
print(s.permutation('abb'))#['abb', 'bab', 'bba']
28.數組中出現次數超過一半的數
數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。
你可以假設數組是非空的,並且給定的數組總是存在多數元素。
示例 :
輸入: [1, 2, 3, 2, 2, 2, 5, 4, 2]
輸出: 2
用sorted()排序后,找中間位
class Solution(object):
def majorityElement(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if not nums:
return None
nums.sort()
return nums[len(nums) //2]
if __name__=='__main__':
s=Solution()
print(s.majorityElement([1, 2, 3, 2, 2, 2, 5, 4, 2]))#2
摩爾投票法:
- 票數和: 由於眾數出現的次數超過數組長度的一半;若記 眾數 的票數為 +1 ,非眾數 的票數為 -1− ,則一定有所有數字的 票數和 > 0 。
- 票數正負抵消: 設數組 nums 中的眾數為 x ,數組長度為 n。若 nums 的前 a個數字的 票數和 = 0 ,則 數組后 (n-a) 個數字的 票數和一定仍 >0 (即后 (n-a)個數字的 眾數仍為 x )
![]()
class Solution:
def majorityElement(self, nums: List[int]) -> int:
votes = 0
for num in nums:
#初始化眾數是第一個數,抵消后設下一個為眾數
if votes == 0:
x = num
#遍歷數組,和眾數相等+1,不相等-1
votes += 1 if num == x else -1
return x
if __name__=='__main__':
s=Solution()
print(s.majorityElement([1, 2, 3, 2, 2, 2, 5, 4, 2]))#2
29.最小的k個數
輸入整數數組 arr ,找出其中最小的 k 個數。例如,輸入4、5、1、6、2、7、3、8這8個數字,則最小的4個數字是1、2、3、4。
示例 :
輸入:arr = [3,2,1], k = 2
輸出:[1,2] 或者 [2,1]
構建大頂堆
堆分為大頂堆和小頂堆,滿足Key[i]>=Key[2i+1]&&key>=key[2i+2]稱為大頂堆,滿足 Key[i]<=key[2i+1]&&Key[i]<=key[2i+2]稱為小頂堆
大頂堆:每個結點的值都大於或等於其左右孩子結點的值。
小頂堆:每個結點的值都小於或等於其左右孩子結點的值一般我們說
topK
問題,就可以用大頂堆或小頂堆來實現,
最大的 K 個:小頂堆
最小的 K 個:大頂堆
- 對於每一個節點而言,他的左孩子節點編號時\(2 * (index+1)\),右孩子節點編號是\(2 * (index+2)\)
- 【非葉子節點下標=節點個數//2-1 :即若root從0開始,則 從 0~節點個數//2-1 這個閉區間范圍內都是非葉子節點,節點個數//2-1之后就是葉子節點了】
- 大頂堆的維護:自底向上的維護,對於葉子節點而言沒有左孩子右孩子,因為大頂堆要求左孩子右孩子都小於父節點,所以不考慮葉子節點,直接從非葉子節點開始.
- 大頂堆的建立總結:從非葉子節點開始維護,維護的過程中根據大頂堆的性質(節點元素大於左右節點元素的值)判斷當前節點應該處於大頂堆的什么位置。
- 取數組前k個元素初始化堆,從最后一個非葉結點開始到根結點來構建最大堆
- 遍歷剩下的n-k個元素
- 當某個元素大於堆頂元素時,直接拋棄
- 當某個元素小於堆頂元素時,替換堆頂元素,再從堆頂重新構建最大堆
class Solution:
def getLeastNumbers(self, arr, k: int):
if not arr or k == 0:
return []
if len(arr) <= k:
return arr
#初始化一個k個元素的堆
heap = arr[:k]
#初始化堆的函數
def buildMaxHeap(pos):
#左節點不能大於構建的堆大小
while pos * 2 + 1 < k:
#初始化最大元素為左孩子
max_pos = pos * 2 + 1
if pos * 2 + 2 < k and heap[pos * 2 + 2] > heap[pos * 2 + 1]:#存在右節點,且右節點大於左節點
max_pos += 1#取最大元素為右孩子的值
#當前節點比最大元素小,交換
if heap[pos] < heap[max_pos]:
heap[pos], heap[max_pos] = heap[max_pos], heap[pos]
pos = max_pos#更新當前節點
else:
break
#模擬k個節點的堆結構,從非葉子節點k // 2-1開始維護
for i in range(k // 2-1, -1, -1):
buildMaxHeap(i)
#遍歷剩下的n-k個數,判斷是否添加進堆
for i in range(k, len(arr)):
#比堆頂元素小,替換堆頂元素,再從堆頂重新構建大頂堆
if arr[i] < heap[0]:
heap[0] = arr[i]
buildMaxHeap(0)
#否則,拋棄
else:
continue
#遍歷完后再返回堆就是
return heap
if __name__=='__main__':
s=Solution()
print(s.getLeastNumbers([1, 5, 3,7,4],2))#【1,3】
30.連續子數組的最大和
輸入一個整型數組,數組中的一個或連續多個整數組成一個子數組。求所有子數組的和的最大值。
要求時間復雜度為O(n)。
示例1:
輸入: nums = [-2,1,-3,4,-1,2,1,-5,4]
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,為 6。
動態規划:
- 狀態定義: 設動態規划列表 dp ,dp[i] 代表以元素 nums[i]為結尾的連續子數組最大和。
- 為何定義最大和 dp[i] 中必須包含元素 nums[i]:保證 dp[i]遞推到 dp[i+1]的正確性;如果不包含 nums[i] ,遞推時則不滿足題目的 連續子數組 要求。
- 轉移方程: 若 \(dp[i-1] \leq 0\),說明 dp[i - 1] 對 dp[i] 產生負貢獻,即 dp[i-1] + nums[i]還不如 nums[i]本身大。
- 當 \(dp[i - 1] > 0\) 時:執行 dp[i] = dp[i-1] + nums[i] ;
當 \(dp[i - 1] \leq 0\) 時:執行 dp[i] = nums[i];
初始狀態: dp[0] = nums[0],即以 nums[0] 結尾的連續子數組最大和為 nums[0] 。- 返回值: 返回 dpdp 列表中的最大值,代表全局最大值。
- 由於 dp[i]dp[i] 只與 dp[i-1]dp[i−1] 和 nums[i]nums[i] 有關系,因此可以將原數組 numsnums 用作 dpdp 列表,即直接在 numsnums 上修改即可。
class Solution:
def maxSubArray(self, nums) -> int:
for i in range(1, len(nums)):
#nums[i-1]>0執行nums[i]+=nums[i-1]nums[i-1]<0執行nums[i]=nums[i],
nums[i] += max(nums[i - 1], 0)
return max(nums)
if __name__=='__main__':
s=Solution()
print(s.maxSubArray( [-2,1,-3,4,-1,2,1,-5,4]))#6
31.從1到n整數中1出現的次數
輸入一個整數 n ,求1~n這n個整數的十進制表示中1出現的次數。
例如,輸入12,1~12這些整數中包含1 的數字有1、10、11和12,1一共出現了5次。
示例 :
輸入:n = 12
輸出:5
根據當前位 cur值的不同,分為以下三種情況:
當 cur = 0 時: 此位 1 的出現次數只由高位 high決定,計算公式為:
high×digit如下圖所示,以 n = 2304 為例,求 digit = 10 (即十位)的 1 出現次數。
![]()
當 cur = 1 時: 此位 1的出現次數由高位 high 和低位 low決定,計算公式為:
\(high×digit+low+1\)如下圖所示,以 n = 2314 為例,求 digit = 10(即十位)的 1出現次數。
![]()
當 cur=2,3,⋯,9 時: 此位 1的出現次數只由高位 high 決定,計算公式為:
\((high+1)×digit\)如下圖所示,以 n = 2324 為例,求 digit = 10(即十位)的 1 出現次數。
![]()
class Solution:
def countDigitOne(self, n: int) -> int:
digit, res = 1, 0
#初始化高位,當前位,低位
high, cur, low = n // 10, n % 10, 0
while high != 0 or cur != 0:
#當前位為0,只與高位和位因子有關
if cur == 0:
res += high * digit
#當前位為1,low+1為當前位開始所有的1的數量,high * digit為高位出現的1的數量
elif cur == 1:
res += high * digit + low + 1
#當前位為其他數,只與高位和位因子有關
else:
res += (high + 1) * digit
low += cur * digit#低位每次是當前位向先進一位
cur = high % 10#當前位每次為上輪高位的最低位
high //= 10#高位每次刪除最低位
digit *= 10#位因子每輪*10
return res
if __name__=='__main__':
s=Solution()
print(s.countDigitOne(12))#5
32.把數組排成最小數
輸入一個非負整數數組,把數組里所有數字拼接起來排成一個數,打印能拼接出的所有數字中最小的一個。
示例 :
輸入: [10,2]
輸出: "102"
雙指針排序
- 把原列表轉換成字符串列表
- 雙指針,i指向前一個數,j指向后一個數
- 有小的排序,交換前后數位置
- 列表轉換成字符串返回
class Solution:
def minNumber(self, nums) -> str:
#把原列表轉換成字符串列表
nums = list(map(str, nums))
#雙指針,指向前一個數,j指向后一個數
for i in range(len(nums) - 1):
for j in range(i + 1, len(nums)):
#有小的排序,交換前后數位置
if nums[i] + nums[j] > nums[j] + nums[i]:
nums[i], nums[j] = nums[j], nums[i]
#列表轉換成字符串返回
return ''.join(nums)
if __name__=='__main__':
s=Solution()
print(s.minNumber([10,2]))#102
快速排序:
- 從無序隊列中挑取一個元素,把無序隊列分割成獨立的兩部分
- 分割:重新排序數列,所有比基准值小的元素擺放在基准前面,所有比基准值大的元素擺在基准后面(與基准值相等的數可以到任何一邊)。在這個分割結束之后,對基准值的排序就已經完成
- 定義兩個游標,分別指向0和末尾位置
- 讓右邊游標往左移動,目的是找到小於mid的值,放到left游標位置
- 讓左邊游標往右移動,目的是找到大於mid的值,放到right游標位置
- 遞歸處理左邊的數據和右邊的數據
class Solution:
def minNumber(self, nums) -> str:
#把原列表轉換成字符串列表
nums = list(map(str, nums))
self.quick_sort(nums, 0, len(nums) - 1)
return ''.join(nums)
#定義快速排序
def quick_sort(self,li, start, end):
# 分治 一分為二
# start=end ,證明要處理的數據只有一個
# start>end ,證明右邊沒有數據
if start >= end:
return
# 定義兩個游標,分別指向0和末尾位置
left = start
right = end
# 把0位置的數據,認為是中間值
mid = li[left]
while left < right:
# 讓右邊游標往左移動,目的是找到小於mid的值,放到left游標位置
while left < right and self.is_larger(li[right],mid):
right -= 1
li[left] = li[right]
# 讓左邊游標往右移動,目的是找到大於mid的值,放到right游標位置
while left < right and self.is_larger(mid,li[left]):
left += 1
li[right] = li[left]
# while結束后,把mid放到中間位置,left=right
li[left] = mid
# 遞歸處理mid左邊的數據
self.quick_sort(li, start, right-1)
# 遞歸mid處理右邊的數據
self.quick_sort(li, left+1 , end)
#區分字符串大小函數
def is_larger(self, str1, str2):
return (str1 + str2) > (str2 + str1)
if __name__=='__main__':
s=Solution()
print(s.minNumber([10,2]))#102
33.丑數
把只包含質因子 2、3 和 5 的數稱作丑數(Ugly Number)。求按從小到大的順序的第 n 個丑數。
示例:
輸入: n = 10
輸出: 12
解釋: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 個丑數
動態規划:
狀態定義: 設動態規划列表 dp,dp[i] 代表第 i + 1個丑數。
轉移方程:
當索引 a, b, c 滿足以下條件時, dp[i]為三種情況的最小值;
每輪計算 dp[i]后,需要更新索引 a, b, c 的值,使其始終滿足方程條件。實現方法:分別獨立判斷 dp[i]和 dp[a]×2 ,dp[b]×3 ,dp[c]×5 的大小關系,若相等則將對應索引 a b , c加 1 。\(dp[a]×2>dp[i−1]≥dp[a−1]×2\)
\(dp[b]×3>dp[i−1]≥dp[b−1]×3\)
\(dp[c]×5>dp[i−1]≥dp[c−1]×5\)
\(dp[i]=min(dp[a]×2,dp[b]×3,dp[c]×5)\)初始狀態: dp[0] = 1,即第一個丑數為 1;
返回值: dp[n-1] ,即返回第 n個丑數
![]()
class Solution:
def nthUglyNumber(self, n: int) -> int:
#初始化前n位丑數都為1
dp, a, b, c = [1] *n, 0, 0, 0
for i in range(1, n):
#dp[i]是滿足丑數索引情況下的最小值
n2, n3, n5 = dp[a] * 2, dp[b] * 3, dp[c] * 5
dp[i] = min(n2, n3, n5)
#更新索引
if dp[i] == n2: a += 1
if dp[i] == n3: b += 1
if dp[i] == n5: c += 1
#索引的第n-1位是第n位丑數
return dp[-1]
if __name__=='__main__':
s=Solution()
print(s.nthUglyNumber(10))#12
34.第一個只出現一次的字符
在字符串 s 中找出第一個只出現一次的字符。如果沒有,返回一個單空格。 s 只包含小寫字母。
示例:
s = "abaccdeff"
返回 "b"
s = ""
返回 " "
哈希表:
- 遍歷字符串
s
,使用哈希表統計 “各字符數量是否 > 1>1 ”。- 再遍歷字符串
s
,在哈希表中找到首個 “數量為 11 的字符”,並返回
class Solution:
def firstUniqChar(self, s: str) -> str:
if not s:
return -1
#使用字典存儲字符數量
count = {}
for i in s:
if i not in count:
count[i] = 1
else:
count[i]+=1
#遍歷字典,找到數量為1
for j in count:
if count.get(j) == 1 :
return j
return ''
if __name__=='__main__':
s=Solution()
print(s.firstUniqChar("abaccdeff"))#b
35.數組中的逆序對
在數組中的兩個數字,如果前面一個數字大於后面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數。
示例 :
輸入: [7,5,6,4]
輸出: 5
歸並排序:
- 歸並排序的過程會將左右數組都變成有序的升序數組
- 左數組當前值比右數組當前值小,左數組前面的值都比右數組當前值小,無逆序對
- 左數組當前值比右數組當前值大,左數組之后的值都比右數組當前值大,都是逆序,逆序數為len(left)-i
class Solution:
def reversePairs(self, nums) -> int:
if not nums:
return 0
l = 0
r = len(nums) - 1
self.cnt = 0
def merge(l, r):
#當待排序序列中只剩下一個數字時,也就是l == r,終止遞歸
if l == r:
return [nums[l]]
#從底層逐步向上合並
else:
mid = (r - l) // 2 + l
left = merge(l, mid)
right = merge(mid + 1, r)
i = j = 0
ans = []
while i < len(left) and j < len(right):
#左數組當前值比右數組當前值小,左數組前面的值都比右數組當前值小,無逆序對
if left[i] <= right[j]:
ans.append(left[i])
i += 1
#左數組當前值比右數組當前值大,左數組之后的值都比右數組當前值大,都是逆序,逆序數為len(left)-i
else:
self.cnt += len(left) - i
ans.append(right[j])
j += 1
if i != len(left):
ans += left[i:]
if j != len(right):
ans += right[j:]
return ans
merge(l, r)
return self.cnt
if __name__=='__main__':
s=Solution()
print(s.reversePairs([7,5,6,4]))#5
36.兩個鏈表第一個公共節點
輸入兩個鏈表,找出它們的第一個公共節點。
如下面的兩個鏈表:
在節點 c1 開始相交。
雙指針:
使用兩個指針 node1,node2 分別指向兩個鏈表 headA,headB 的頭結點,然后同時分別逐結點遍歷,當 node1 到達鏈表 headA 的末尾時,重新定位到鏈表 headB 的頭結點;當 node2 到達鏈表 headB 的末尾時,重新定位到鏈表 headA 的頭結點。
這樣,當它們相遇時,所指向的結點就是第一個公共結點
#定義鏈表
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution():
'''def getIntersectionNode(self, link1: ListNode, link2: ListNode):
if not link1 or not link2:
return None
length1 = length2 = 0
move1, move2 = link1, link2
while move1: # 獲取鏈表長度
length1 += 1
move1 = move1.next
while move2:
length2 += 1
move2 = move2.next
while length1 > length2: # 長鏈表先走多的長度
length1 -= 1
link1 = link1.next
while length2 > length1:
length2 -= 1
link2 = link2.next
while link1: # 鏈表一起走
if link1 == link2:
return link1
link1, link2 = link1.next, link2.next
return None'''
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
node1, node2 = headA, headB
while node1 != node2:
node1 = node1.next if node1 else headB
node2 = node2.next if node2 else headA
return node1
37.數字在排序數組中出現的次數
統計一個數字在排序數組中出現的次數。
示例 :
輸入: nums = [5,7,7,8,8,10], target = 8
輸出: 2
二分查找:
- 初始化: 左邊界 i = 0 ,右邊界 j = len(nums) - 1 。
- 循環二分: 當閉區間 \([i, j]\) 無元素時跳出;
- 計算中點 m = (i + j) / 2(向下取整);
- 若 nums[m] < target,則 target在閉區間 \([m + 1, j]\) 中,因此執行 i = m + 1;
- 若 nums[m] > target ,則 target 在閉區間 \([i, m - 1]\) 中,因此執行 j = m - 1;
- 若 nums[m] = target ,則右邊界 right在閉區間\([m + 1, j]\) 中;左邊界 left 在閉區間 \([m + 1, j]\) 中。因此分為以下兩種情況:
若查找 右邊界 right ,則執行 i = m + 1 ;(跳出時 i指向右邊界)
若查找 左邊界 left ,則執行 j = m - 1 ;(跳出時 j指向左邊界)- 返回值: 應用兩次二分,分別查找 right 和 left ,最終返回 right - left - 1即可
[5,7,7,8,8,10]
38.二叉樹的深度
輸入一棵二叉樹的根節點,求該樹的深度。從根節點到葉節點依次經過的節點(含根、葉節點)形成樹的一條路徑,最長路徑的長度為樹的深度。
例如:
給定二叉樹 [3,9,20,null,null,15,7],
3
/
9 20
/
15 7
后序遍歷(左右根):
- 終止條件: 當 root 為空,說明已越過葉節點,因此返回 深度 00 。
- 遞推工作: 本質上是對樹做后序遍歷。
計算節點 root 的 左子樹的深度 ,即調用 maxDepth(root.left);
計算節點 root 的 右子樹的深度 ,即調用 maxDepth(root.right)- 樹的深度 等於 左子樹的深度 與 右子樹的深度 中的 最大值 +1
![]()
#定義樹:
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root: return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
#生成樹
node1=TreeNode(5)
node1.left=TreeNode(9)
node1.right=TreeNode(20)
node1.right.left=TreeNode(15)
node1.right.right=TreeNode(7)
s=Solution()
print(s.maxDepth(node1))#3
39.判斷是否是平衡二叉樹
輸入一棵二叉樹的根節點,判斷該樹是不是平衡二叉樹。如果某二叉樹中任意節點的左右子樹的深度相差不超過1,那么它就是一棵平衡二叉樹。
示例 1:
給定二叉樹 [3,9,20,null,null,15,7]
3
/
9 20
/
15 7
后序遍歷+剪枝
recur(root) 函數:
- 返回值:
當節點root 左 / 右子樹的深度差≤1 :則返回當前子樹的深度,即節點 root 的左 / 右子樹的深度最大值 +1 ( max(left, right) + 1 );
當節點root 左 / 右子樹的深度差 > 2:則返回 -1,代表 此子樹不是平衡樹 。- 終止條件:
當 root 為空:說明越過葉節點,因此返回高度 0 ;
當左(右)子樹深度為 -1 :代表此樹的 左(右)子樹 不是平衡樹,因此剪枝,直接返回 -1
#定義樹:
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
def recur(root):
if not root: return 0
left = recur(root.left)
if left == -1: return -1
right = recur(root.right)
if right == -1: return -1
return max(left, right) + 1 if abs(left - right) <= 1 else -1
return recur(root) != -1
#生成樹
node1=TreeNode(5)
node1.left=TreeNode(9)
node1.right=TreeNode(20)
node1.right.left=TreeNode(15)
node1.right.right=TreeNode(7)
s=Solution()
print(s.isBalanced(node1))#True
40.數組中只出現一次的數字
一個整型數組 nums 里除兩個數字之外,其他數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。要求時間復雜度是O(n),空間復雜度是O(1)。
示例 :
輸入:nums = [4,1,4,6]
輸出:[1,6] 或 [6,1]
位運算
異或的性質
兩個數字異或的結果a^b
是將 a 和 b 的二進制每一位進行運算,得出的數字。 運算的邏輯是
如果同一位的數字相同則為 0,不同則為 1
- 先對所有數字進行一次異或,得到兩個出現一次的數字的異或值。
- 在異或結果中找到任意為 1 的位。
- 根據這一位對所有的數字進行分組。
- 在每個組內進行異或操作,得到兩個數字
import functools
class Solution:
def singleNumbers(self, nums) :
#先對所有數字進行一次異或,得到兩個出現一次的數字的異或值
ret = functools.reduce(lambda x, y: x ^ y, nums)
div = 1
#rec位是0while不結束,繼續左移位
while div & ret == 0:
div <<= 1
#找到第一個為1的位
a, b = 0, 0
#a,b對應不同數字
for n in nums:
#與這位同為1的分到同一組,相同的數字被分到一組
if n & div:
a ^= n
#與這位同為0的分到同一組,相同的數字被分到一組
else:
b ^= n
return [a, b]
if __name__=='__main__':
s=Solution()
print(s.singleNumbers(nums = [4,1,4,6]))#[1, 6]
41.和為s的兩個數字
輸入一個遞增排序的數組和一個數字s,在數組中查找兩個數,使得它們的和正好是s。如果有多對數字的和等於s,則輸出任意一對即可。
示例 :
輸入:nums = [2,7,11,15], target = 9
輸出:[2,7] 或者 [7,2]
對撞雙指針:
- 初始化: 雙指針 i , j分別指向數組 nums的左右兩端 (俗稱對撞雙指針)。
- 循環搜索: 當雙指針相遇時跳出;
- 計算和 s = nums[i] + nums[j];
若 s > targets ,則指針 j向左移動,即執行 j = j - 1;
若 s < targets ,則指針 i向右移動,即執行 i = i + 1;
若 s = targets ,立即返回數組 [nums[i], nums[j]] ;- 返回空數組,代表無和為 target的數字組合
class Solution:
def twoSum(self, nums, target):
i, j = 0, len(nums) - 1
while i < j:
s = nums[i] + nums[j]
if s > target: j -= 1
elif s < target: i += 1
else: return nums[i], nums[j]
return []
if __name__=='__main__':
s=Solution()
print(s.twoSum([2,7,11,15],9))#(2, 7)
42.和為s的連續正數序列
輸入一個正整數 target ,輸出所有和為 target 的連續正整數序列(至少含有兩個數)。
序列內的數字由小到大排列,不同序列按照首個數字從小到大排列。
示例 :
輸入:target = 9
輸出:[[2,3,4],[4,5]]
滑動窗口:
- 當窗口的和小於 target 的時候,窗口的和需要增加,所以要擴大窗口,窗口的右邊界向右移,窗口多一個j值,要加上
- 當窗口的和大於 target 的時候,窗口的和需要減少,所以要縮小窗口,窗口的左邊界向右移,窗口少一個i值,要減去
- 當窗口的和恰好等於 target 的時候,我們需要記錄此時的結果。設此時的窗口為 [i, j),那么我們已經找到了一個 i開頭的序列,也是唯一一個 i開頭的序列,接下來需要找 i+1 開頭的序列,所以窗口的左邊界要向右移動
class Solution:
def findContinuousSequence(self, target: int):
i = 1 # 滑動窗口的左邊界
j = 1 # 滑動窗口的右邊界
sum = 0 # 滑動窗口中數字的和
res = []
#必須有兩個數,左邊界比target // 2大,+右邊界(至少左邊界+1)一定比target大
while i <= target // 2:
if sum < target:
# 右邊界向右移動
sum += j
j += 1
elif sum > target:
# 左邊界向右移動
sum -= i
i += 1
else:
# 記錄結果
arr = list(range(i, j))
res.append(arr)
# 左邊界向右移動
sum -= i
i += 1
return res
if __name__=='__main__':
s=Solution()
print(s.findContinuousSequence(9))#[[2, 3, 4], [4, 5]]
43.翻轉單詞順序
輸入一個英文句子,翻轉句子中單詞的順序,但單詞內字符的順序不變。為簡單起見,標點符號和普通字母一樣處理。例如輸入字符串"I am a student. ",則輸出"student. a am I"。
示例 :
輸入: "the sky is blue"
輸出: "blue is sky the"
雙指針:
- 倒序遍歷字符串 s ,記錄單詞左右索引邊界 i , j;
- 每確定一個單詞的邊界,則將其添加至單詞列表 res;
- 索引i從右往左搜索首個空格
- 添加單詞:首個空格之后的位置到右邊界的位置
- I搜索下一個不是空格的位置,找到單詞的右邊界,將下個單詞右邊界j設置成i
- 最終,將單詞列表拼接為字符串,並返回即可
class Solution:
def reverseWords(self, s: str) -> str:
s = s.strip() # 刪除首尾空格
i = j = len(s) - 1#倒序遍歷字符串
res = []
while i >= 0:
while i >= 0 and s[i] != ' ':
i -= 1 # 搜索首個空格
res.append(s[i + 1: j + 1]) # 添加單詞
while s[i] == ' ':
i -= 1 # 跳過單詞間空格
j = i # j 指向下個單詞的尾字符
return ' '.join(res) # 拼接並返回
if __name__=='__main__':
s=Solution()
print(s.reverseWords("the sky is blue"))#blue is sky the
44.左旋轉字符串
字符串的左旋轉操作是把字符串前面的若干個字符轉移到字符串的尾部。請定義一個函數實現字符串左旋轉操作的功能。比如,輸入字符串"abcdefg"和數字2,該函數將返回左旋轉兩位得到的結果"cdefgab"。
示例 :
輸入: s = "abcdefg", k = 2
輸出: "cdefgab"
切片:
獲取字符串 s[n:]s[n:] 切片和 s[:n]s[:n] 切片,使用 "++" 運算符拼接並返回即可
class Solution:
def reverseLeftWords(self, s: str, n: int) -> str:
return s[n:] + s[:n]
if __name__=='__main__':
s=Solution()
print(s.reverseLeftWords("abcdefg",2))#cdefgab
45.n個骰子的點數
把n個骰子扔在地上,所有骰子朝上一面的點數之和為s。輸入n,打印出s的所有可能的值出現的概率。
你需要用一個浮點數數組返回答案,其中第 i 個元素代表這 n 個骰子所能擲出的點數集合中第 i 小的那個的概率。
示例 :
輸入: 1
輸出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
動態規划:
- n個骰子,一共有6**n種情況
- n=1, 和為s的情況有 F(n,s)=1 s=1,2,3,4,5,6
- n>1 , F(n,s) = F(n-1,s-1)+F(n-1,s-2) +F(n-1,s-3)+F(n-1,s-4)+F(n-1,s-5)+F(n-1,s-6)
可以看作是從前(n-1)個骰子投完之后的狀態轉移過來。
其中F(N,S)表示投第N個骰子時,點數和為S的次數
class Solution:
def twoSum(self, n: int):
#初始化二維數組,行為n個骰子,列為n個骰子一面朝上的點數和有6*n+1種可能
dp = [[0 for j in range(6*n+1)]for i in range(n+1)]
#初始化0個骰子,和為0的次數為1
dp[0][0]=1
#第n個骰子
for i in range(1,n+1):
#第n個骰子點數和為6*n
for j in range(i,6*i+1):
for k in range(1,7):
if j-k>=0:
#F(n,s) = F(n-1,s-1)+F(n-1,s-2) +F(n-1,s-3)+F(n-1,s-4)+F(n-1,s-5)+F(n-1,s-6)
dp[i][j]+=dp[i-1][j-k]
print(dp)#[[1, 0, 0, 0, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1]]
#dp[n][n:]第n個骰子,和為s的次數,x從中取和/總的和的可能(1/6)**n
res = list(map(lambda x:x*(1/6)**n,dp[n][n:]))
return res
if __name__=='__main__':
s=Solution()
print(s.twoSum(1))#[0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
45.撲克牌中的順子
從撲克牌中隨機抽5張牌,判斷是不是一個順子,即這5張牌是不是連續的。2~10為數字本身,A為1,J為11,Q為12,K為13,而大、小王為 0 ,可以看成任意數字。A 不能視為 14。
示例 :
輸入: [1,2,3,4,5]
輸出: True
集合:
- 除大小王外,所有牌 無重復 ;
- 設此 55 張牌中最大的牌為 max,最小的牌為 min (大小王除外),則需滿足:
max - min < 5
max−min<5![]()
class Solution:
def isStraight(self, nums) -> bool:
repeat = set()
ma, mi = 0, 14
for num in nums:
if num == 0: continue # 跳過大小王
ma = max(ma, num) # 最大牌
mi = min(mi, num) # 最小牌
if num in repeat:
return False # 若有重復,提前返回 false
repeat.add(num) # 添加牌至 Set
return ma-mi<5
if __name__=='__main__':
s=Solution()
print(s.isStraight([1,2,3,4,5]))#True
46.圓圈中最后剩下的數字
0,1,,n-1這n個數字排成一個圓圈,從數字0開始,每次從這個圓圈里刪除第m個數字。求出這個圓圈里剩下的最后一個數字。
例如,0、1、2、3、4這5個數字組成一個圓圈,從數字0開始每次刪除第3個數字,則刪除的前4個數字依次是2、0、4、1,因此最后剩下的數字是3。
示例 :
輸入: n = 5, m = 3
輸出: 3
遞歸:
- 找到f(n,start=0)和f(n-1,start=0)的關系
- 從 f(n - m) 場景下刪除的第一個數的序號是 (m - 1) % n,記為k,那么 f(n - 1, m) 場景將使用被刪除數字的下一個數,即序號 m % n 作為它的 0 序號,記為k+1
- f(n,start=0)=f(n-1,start = k+1)=(f(n-1,start=0)+k+1)=(f(n-1,start=0)+m%n)
- 設
f(n - 1, m)
的結果為x
,f(n-1,start=0)x即則f(n,start=0)=x+m%n- 由於
m % n + x
可能會超過 n 的范圍,所以我們再取一次模f(n , m) = (m % n + x) % n = (m + x) % n
class Solution:
def lastRemaining(self, n: int, m: int) -> int:
return self.f(n, m)
def f(self, n, m):
if n == 0:
return 0
#設 f(n - 1, m) 的結果為 x
x = self.f(n - 1, m)
#f(n,m)的結果為(x+f(n - 1, m)開始序列m%n)取模%n
return (m + x) % n
if __name__=='__main__':
s=Solution()
print(s.lastRemaining(5,3))#3
47.求1+2+...n
求 1+2+...+n ,要求不能使用乘除法、for、while、if、else、switch、case等關鍵字及條件判斷語句(A?B:C)。
示例 :
輸入: n = 3
輸出: 6
遞歸+邏輯運算符的短路:
- 常見的邏輯運算符有三種,即 “與 and”,“或 or”,“非 ! ” ;而其有重要的短路效應,如下所示:
if(A and B) // 若 A 為 false ,則 B 的判斷不會執行(即短路),直接判定 A and B 為 false
if(A or B) // 若 A 為 true ,則 B 的判斷不會執行(即短路),直接判定 A or B 為 true
- n > 1 and sumNums(n - 1) // 當 n = 1 時 n > 1 不成立 ,此時 “短路” ,終止后續遞歸
class Solution:
def __init__(self):
self.res = 0
def sumNums(self, n: int) -> int:
#n=1時終止遞歸
n > 1 and self.sumNums(n - 1)
self.res += n
return self.res
#return reduce(lambda x,y: x+y, range(1,n+1))
if __name__=='__main__':
s=Solution()
print(s.lastRemaining(5,3))#3
48.不用加減乘除做加法
寫一個函數,求兩個整數之和,要求在函數體內不得使用 “+”、“-”、“*”、“/” 四則運算符號。
示例:
輸入: a = 1, b = 1
輸出: 2
位運算:
設兩數字的二進制形式 a, b ,其求和 s = a + b ,a(i)代表 a 的二進制第 i位,則分為以下四種情況:
a(i) b(i) 無進位和 n(i) 進位 c(i+1) 00 00 00 00 00 11 11 00 11 00 11 00 11 11 00 11
- 觀察發現,無進位和 與 異或運算 規律相同(相同為0,不同為1,進位 和 與運算 規律相同(並需左移一位)。因此,無進位和 n 與進位 c 的計算公式如下;
\[\begin{cases} n=a⊕b\\ c=a\&b<<1 \end{cases} \]
即可將 s = a + b 轉化為:\(s = a + b \Rightarrow s = n + c\)
循環求 n 和 c ,直至進位 c = 0 ;此時 s = n ,返回 n即可
獲取負數的補碼: 需要將數字與十六進制數 0xffffffff 相與。可理解為舍去此數字 32 位以上的數字(將 32 位以上都變為 0 ),從無限長度變為一個 32 位整數。
返回前數字還原: 若補碼 a為負數( 0x7fffffff 是最大的正數的補碼 ),需執行 ~(a ^ x) 操作,將補碼還原至 Python 的存儲格式。 a ^ x 運算將 1 至 32 位按位取反; ~ 運算是將整個數字取反;因此, ~(a ^ x) 是將 32 位以上的位取反,1 至 32 位不變
class Solution:
def add(self, a: int, b: int) -> int:
x = 0xffffffff
a, b = a & x, b & x
#進位為0,跳出循環
while b != 0:
#非進位和進位
a, b = (a ^ b), (a & b) << 1 & x
#結果為負數,還原補碼
return a if a <= 0x7fffffff else ~(a ^ x)
if __name__=='__main__':
s=Solution()
print(s.add(5,-7))#-2
49.把字符串轉換成整數
寫一個函數 StrToInt,實現把字符串轉換成整數這個功能。不能使用 atoi 或者其他類似的庫函數。
首先,該函數會根據需要丟棄無用的開頭空格字符,直到尋找到第一個非空格的字符為止。
當我們尋找到的第一個非空字符為正或者負號時,則將該符號與之后面盡可能多的連續數字組合起來,作為該整數的正負號;假如第一個非空字符是數字,則直接將其與之后連續的數字字符組合起來,形成整數。
該字符串除了有效的整數部分之后也可能會存在多余的字符,這些字符可以被忽略,它們對於函數不應該造成影響。
注意:假如該字符串中的第一個非空格字符不是一個有效整數字符、字符串為空或字符串僅包含空白字符時,則你的函數不需要進行轉換。
在任何情況下,若函數不能進行有效的轉換時,請返回 0。
說明:
假設我們的環境只能存儲 32 位大小的有符號整數,那么其數值范圍為 [−231, 231 − 1]。如果數值超過這個范圍,請返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
示例 1:
輸入: "42"
輸出: 42
首部空格: 刪除之即可;
符號位: 三種情況,即 ''+'' , ''−'' , ''無符號" ;新建一個變量保存符號位,返回前判斷正負即可。
非數字字符: 遇到首個非數字的字符時,應立即返回。
數字字符:
字符轉數字: “此數字的 ASCII 碼” 與 “ 00 的 ASCII 碼” 相減即可;
數字拼接: 若從左向右遍歷數字,設當前位字符為 c,當前位數字為 x ,數字結果為 res ,則數字拼接公式為:
\(res = 10 \times res + x\)\(x = ascii(c) - ascii('0')\)
在每輪數字拼接前,判斷 resres 在此輪拼接后是否超過 2147483647 ,若超過則加上符號位直接返回。
設數字拼接邊界 bndry = 2147483647 // 10 = 214748364 ,則以下兩種情況越界:\[\begin{cases} res > bndry & 情況一:執行拼接 10 \times res \geq 2147483650 越界 \\ res = bndry, x > 7 & 情況二:拼接后是 2147483648 或 2147483649 越界 \\ \end{cases} \]
class Solution:
def strToInt(self, str: str) -> int:
str = str.strip() # 刪除首尾空格
if not str: return 0 # 字符串為空則直接返回
res, i, sign = 0, 1, 1
int_max, int_min, bndry = 2 ** 31 - 1, -2 ** 31, 2 ** 31 // 10
if str[0] == '-': sign = -1 # 保存負號
elif str[0] != '+': i = 0 # 若無符號位,需從 i = 0 開始數字拼接
for c in str[i:]:
if not '0' <= c <= '9' : break # 遇到非數字的字符則跳出
if res > bndry or res == bndry and c > '7':
return int_max if sign == 1 else int_min # 數字越界處理
res = 10 * res + ord(c) - ord('0') # 數字拼接
return sign * res
if __name__=='__main__':
s=Solution()
print(s.strToInt("-42l56j90"))#-42
50.樹中兩個節點的公共節點
給定一個二叉樹, 找到該樹中兩個指定節點的最近公共祖先。
百度百科中最近公共祖先的定義為:“對於有根樹 T 的兩個結點 p、q,最近公共祖先表示為一個結點 x,滿足 x 是 p、q 的祖先且 x 的深度盡可能大(一個節點也可以是它自己的祖先)。”
例如,給定如下二叉樹: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 :
輸入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
輸出: 3
解釋: 節點 5 和節點 1 的最近公共祖先是節點 3。
祖先的定義: 若節點 p 在節點 root 的左(右)子樹中,或 p = root,則稱 root 是 p 的祖先。
![]()
最近公共祖先的定義: 設節點 root為節點 p, q的某公共祖先,若其左子節點 root.left 和右子節點 root.right都不是 p,q 的公共祖先,則稱 root 是 “最近的公共祖先” 。
![]()
根據以上定義,若 root 是 p, q 的 最近公共祖先 ,則只可能為以下情況之一:
- p 和 q在 root 的子樹中,且分列 root 的 異側(即分別在左、右子樹中);
- p = root ,且 q 在 root 的左或右子樹中(q 在 root 的左或右子樹中,root是q的祖先,p = root,root是的祖先,所以root是公共祖先);
- q = root ,且 p 在 root的左或右子樹中
- 終止條件:
當越過葉節點,則直接返回 null ;
當 root 等於 p, q,則直接返回 root ;- 遞推工作:
開啟遞歸左子節點,返回值記為 left ;
開啟遞歸右子節點,返回值記為 right ;- 返回值: 根據 left和 righ ,可展開為四種情況;
- 當 left 和 right 同時為空 :說明 root 的左 / 右子樹中都不包含 p,q ,返回 null;
- 當 leftl 和 right 同時不為空 :說明 p, q分列在 root 的 異側 (分別在 左 / 右子樹),因此 root 為最近公共祖先,返回 root;
- 當 left 為空 ,right 不為空 :p,q都不在 root的左子樹中,直接返回 righ 。具體可分為兩種情況:
p,q 其中一個在 root的 右子樹 中,此時 right指向 p(假設為 p);
p,q 兩節點都在 root 的 右子樹 中,此時的 right 指向 最近公共祖先節點 ;
當 left不為空 , right為空 :與情況 3. 同理;
class TreeNode:
def __init__(self, x):
self.val = x
self.left = None
self.right = None
class Solution:
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
if not root or root == p or root == q:#不為空,且有一個點是最近公共祖先
return root
left = self.lowestCommonAncestor(root.left, p, q)#遞歸左子樹
right = self.lowestCommonAncestor(root.right, p, q)#遞歸右子樹
if not left and not right:
return # root 的左 / 右子樹中都不包含 p,q
if not left:
return right # p,q都不在 root的左子樹中.
if not right:
return left # p,q都不在 root的右子樹中.
return root # p, q分列在 root 的 異側,root 為最近公共祖先
補充
1.環形鏈表入口
給定一個鏈表,返回鏈表開始入環的第一個節點。 如果鏈表無環,則返回 null。
為了表示給定鏈表中的環,我們使用整數 pos 來表示鏈表尾連接到鏈表中的位置(索引從 0 開始)。 如果 pos 是 -1,則在該鏈表中沒有環。
說明:不允許修改給定的鏈表。
示例 1:
輸入:head = [3,2,0,-4], pos = 1
輸出:tail connects to node index 1
解釋:鏈表中有一個環,其尾部連接到第二個節點。
快慢指針:
雙指針第一次相遇: 設兩指針 fast,slow 指向鏈表頭部 head,fast 每輪走 2 步,slow 每輪走 1步;
第一種結果: fast 指針走過鏈表末端,說明鏈表無環,直接返回 null;
TIPS: 若有環,兩指針一定會相遇。因為每走 11 輪,fast 與 slow 的間距 +1+1,fast 終會追上 slow;
第二種結果: 當fast == slow時, 兩指針在環中 第一次相遇 。下面分析此時fast 與 slow走過的 步數關系 :
- 設鏈表共有 a+b個節點,其中 鏈表頭部到鏈表入口 有 a 個節點(不計鏈表入口節點), 鏈表環 有 b個節點(這里需要注意,a 和 b是未知數,例如圖解上鏈表 a=4 b=5);
- 設兩指針分別走了 f,s 步,則有:fast 走的步數是slow步數的 2倍,即 f = 2s;(解析: fast 每輪走 2 步)
fast 比 slow多走了 n個環的長度,即 f = s + nb;( 解析: 雙指針都走過 a步,然后在環內繞圈直到重合,重合時 fast 比 slow 多走 環的長度整數倍 );
以上兩式相減得:f = 2nb,s = nb,即fast和slow 指針分別走了 2n,n個 環的周長 (注意: n是未知數,不同鏈表的情況不同)。- 如果讓指針從鏈表頭部一直向前走並統計步數k,那么所有 走到鏈表入口節點時的步數 是:k=a+nb(先走 a 步到入口節點,之后每繞 1 圈環( b步)都會再次到入口節點)。
而目前,slow 指針走過的步數為 nb 步。因此,我們只要想辦法讓 slow 再走 a 步停下來,就可以到環的入口。
雙指針第二次相遇:
- slow指針 位置不變 ,將fast指針重新 指向鏈表頭部節點 ;slow和fast同時每輪向前走 1 步;
- TIPS:此時 f = 0,s = nb ;
- 當 fast 指針走到f = a 步時,slow 指針走到步s = a+nb,此時 兩指針重合,並同時指向鏈表環入口 。
![]()
- 返回slow指針指向的節點。
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution(object):
def detectCycle(self, head):
fast, slow = head, head
while True:
#fast走過鏈表末端,無環
if not (fast and fast.next): return
#fast走兩步,slow走一步
fast, slow = fast.next.next, slow.next
#快慢指針相遇,f=2s=s+nb=2nb,慢指針走nb(b表示環節點)
if fast == slow: break
#fast指向鏈表頭節點0
fast = head
while fast != slow:
#fast=a,slow=a+nb時相遇,相遇點為a點(環入口)
fast, slow = fast.next, slow.next
return fast
2.LRU緩存機制
設計LRU緩存結構,該結構在構造時確定大小,假設大小為K,並有如下兩個功能
- set(key, value):將記錄(key, value)插入該結構
- get(key):返回key對應的value值
[要求]
- set和get方法的時間復雜度為O(1)
- 某個key的set或get操作一旦發生,認為這個key的記錄成了最常使用的。
- 當緩存的大小超過K時,移除最不經常使用的記錄,即set或get最久遠的。
若opt=1,接下來兩個整數x, y,表示set(x, y)
若opt=2,接下來一個整數x,表示get(x),若x未出現過或已被移除,則返回-1
對於每個操作2,輸出一個答案
示例1
輸入
[[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3
輸出
[1,-1]
說明
第一次操作后:最常使用的記錄為("1", 1)
第二次操作后:最常使用的記錄為("2", 2),("1", 1)變為最不常用的
第三次操作后:最常使用的記錄為("3", 2),("1", 1)還是最不常用的
第四次操作后:最常用的記錄為("1", 1),("2", 2)變為最不常用的
第五次操作后:大小超過了3,所以移除此時最不常使用的記錄("2", 2),加入記錄("4", 4),並且為最常使用的記錄,然后("3", 2)變為最不常使用的記錄
LRU 緩存機制可以通過哈希表輔以雙向鏈表實現,我們用一個哈希表和一個雙向鏈表維護所有在緩存中的鍵值對。
雙向鏈表按照被使用的順序存儲了這些鍵值對,靠近頭部的鍵值對是最近使用的,而靠近尾部的鍵值對是最久未使用的。
哈希表即為普通的哈希映射(HashMap),通過緩存數據的鍵映射到其在雙向鏈表中的位置。
這樣以來,我們首先使用哈希表進行定位,找出緩存項在雙向鏈表中的位置,隨后將其移動到雙向鏈表的頭部,即可在 O(1)的時間內完成 get 或者 put 操作。具體的方法如下:
- 對於 get 操作,首先判斷 key 是否存在:
- 如果 key 不存在,則返回 -1;
- 如果 key 存在,則 key 對應的節點是最近被使用的節點。通過哈希表定位到該節點在雙向鏈表中的位置,並將其移動到雙向鏈表的頭部,最后返回該節點的值。
- 對於 put 操作,首先判斷 key 是否存在:
- 如果 key 不存在,使用 key 和 value 創建一個新的節點,在雙向鏈表的頭部添加該節點,並將 key 和該節點添加進哈希表中。然后判斷雙向鏈表的節點數是否超出容量,如果超出容量,則刪除雙向鏈表的尾部節點,並刪除哈希表中對應的項;
- 如果 key 存在,則與 get 操作類似,先通過哈希表定位,再將對應的節點的值更新為 value,並將該節點移到雙向鏈表的頭部。
#
# lru design
# @param operators int整型二維數組 the ops
# @param k int整型 the k
# @return int整型一維數組
#
#定義雙向鏈表
class Node:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.next = None
self.prev = None
class Solution:
#初始化存儲為0,
def __init__(self):
self.size = 0
#雙向鏈表靠近頭部的鍵值對是最近使用的,而靠近尾部的鍵值對是最久未使用的
self.head = Node()
self.tail = Node()
self.head.next = self.tail
self.tail.prev = self.head
#hash表:通過緩存數據的鍵映射到其在雙向鏈表中的位置
self.hash_map = {}
def put(self, key, value, k):
# key存在
if key in self.hash_map:
#設置新的鍵值
self.hash_map[key].value = value
#將該鍵在鏈表中位置刪除,移到鏈表頭部最常使用的
self.move_to_head(self.hash_map[key])
# key not exists
else:
# 緩存大小小於k
if self.size < k:
#設置的值映射到其在雙向鏈表中的位置
self.hash_map[key] = Node(key, value)
#設置的值存儲到hash表頭中,成為最常使用記錄
self.add_head(self.hash_map[key])
#緩存大小加1
self.size += 1
# 緩存大小大於k
else:
#移除鏈表末尾最不常使用的記錄
self.remove_node(self.tail.prev)
self.hash_map[key] = Node(key, value)
self.add_head(self.hash_map[key])
def get(self, key):
# key not exists
if key not in self.hash_map:
return -1
# key存在,該鍵在鏈表中位置刪除,移到鏈表頭部成最常使用的
self.move_to_head(self.hash_map[key])
#返回鍵值
return self.hash_map[key].value
#記錄移到鏈表頭部成最常使用的
def move_to_head(self, node):
#刪除記錄在鏈表中原來位置
node.prev.next = node.next
node.next.prev = node.prev
#記錄放到head和原來head.next中間,使之成為新的鏈表頭部
node.next = self.head.next
node.prev = self.head
#保存記錄,更新鏈表
self.head.next = node
node.next.prev = node
#記錄存到鏈表頭部
def add_head(self, node):
node.next = self.head.next
node.prev = self.head
self.head.next = node
node.next.prev = node
def remove_node(self, node):
node.next.prev = node.prev
node.prev.next = node.next
self.hash_map.pop(node.key)
def LRU(self, operators, k):
# write code here
res = []
for i in range(len(operators)):
if operators[i][0] == 1:
self.put(operators[i][1], operators[i][2], k)
else:
res.append(self.get(operators[i][1]))
return res
s=Solution()
print(s.LRU([[1,1,1],[1,2,2],[1,3,2],[2,1],[1,4,4],[2,2]],3))#[1, -1]
3.不同的子序列
給定一個字符串 S 和一個字符串 T,計算在 S 的子序列中 T 出現的個數。
一個字符串的一個子序列是指,通過刪除一些(也可以不刪除)字符且不干擾剩余字符相對位置所組成的新字符串。(例如,"ACE" 是 "ABCDE" 的一個子序列,而 "AEC" 不是)
題目數據保證答案符合 32 位帶符號整數范圍。
示例 :
輸入:S = "rabbbit", T = "rabbit"
輸出:3
解釋:
如下圖所示, 有 3 種可以從 S 中得到 "rabbit" 的方案。
(上箭頭符號 ^ 表示選取的字母)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
動態規划:
- 設定
dp[i][j]
為t
的前i
個字符可以由s
的前j
個字符組成多少個- 當
s[j] == t[i]
時:
- 取S[j],那么當前情況總數,應該和字符串S的前j-1個字符所構成的子序列中出現字符串T的前i-1個字符的情況總數相等。
- 不取S[j],那么當前情況總數,應該和字符串S的前j-1個字符所構成的子序列中出現字符串T的前i個字符的情況總數相等。
- 轉移方程為
dp[i][j] = dp[i-1][j-1] + dp[i][j-1]
- 當
s[j] != t[i]
時和不取S[j]的情況相同,轉移方程為dp[i][j] = dp[i][j-1]
class Solution():
def numDistinct(self, s: str, t: str) -> int:
n1 = len(s)
n2 = len(t)
#dp[i][j]為t的前i個字符可以由s的前j個字符組成的最多個數
dp = [[0] * (n1 + 1) for _ in range(n2 + 1)]
for j in range(n1 + 1):
#初始化t第一個元素是空時,都是s子序列,所以第一行都是1
dp[0][j] = 1
for i in range(1, n2 + 1):
for j in range(1, n1 + 1):
#i-1是n2,t的最后一個元素
if t[i - 1] == s[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1]
else:
dp[i][j] = dp[i][j - 1]
#print(dp)
return dp[-1][-1]
if __name__=='__main__':
s=Solution()
print(s.numDistinct("rabbbit","rabbit"))#3