21-100、相同的树
给定两个二叉树,编写一个函数来检验它们是否相同。
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例 1:
输入: 1 1
/ \ / \
2 3 2 3
[1,2,3], [1,2,3]
输出: true
递归:
时间复杂度 : O(N)O(N),其中 N 是树的结点数,因为每个结点都访问一次。
空间复杂度 : 最优情况(完全平衡二叉树)时为 O(\log(N))O(log(N)),最坏情况下(完全不平衡二叉树)时为 {O}(N)O(N),用于维护递归栈。
class Solution:
def isSameTree(self, p, q):
"""
:type p: TreeNode
:type q: TreeNode
:rtype: bool
"""
# p and q are both None
if not p and not q:
return True
# one of p and q is None
if not q or not p:
return False
if p.val != q.val:
return False
return self.isSameTree(p.right, q.right) and \
self.isSameTree(p.left, q.left)
22-101、对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
递归实现
乍一看无从下手,但用递归其实很好解决。
根据题目的描述,镜像对称,就是左右两边相等,也就是左子树和右子树是相当的。
注意这句话,左子树和右子相等,也就是说要递归的比较左子树和右子树。
我们将根节点的左子树记做left,右子树记做right。比较left是否等于right,不等的话直接返回就可以了。
如果相当,比较left的左节点和right的右节点,再比较left的右节点和right的左节点
比如看下面这两个子树(他们分别是根节点的左子树和右子树),能观察到这么一个规律:
左子树2的左孩子 == 右子树2的右孩子
左子树2的右孩子 == 右子树2的左孩子
2 2
/ \ / \
3 4 4 3
/ \ / \ / \ / \
8 7 6 5 5 6 7 8
根据上面信息可以总结出递归函数的两个条件:
终止条件:
left和right不等,或者left和right都为空
递归的比较left.left和right.right,递归比较left.right和right.left
class Solution(object):
def isSymmetric(self, root):
"""
:type root: TreeNode
:rtype: bool
"""
if not root:
return True
def dfs(left,right):
# 递归的终止条件是两个节点都为空
# 或者两个节点中有一个为空
# 或者两个节点的值不相等
if not (left or right):
return True
if not (left and right):
return False
if left.val!=right.val:
return False
return dfs(left.left,right.right) and dfs(left.right,right.left)
# 用递归函数,比较左节点,右节点
return dfs(root.left,root.right)
23-104、二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最大深度 3 。
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if root== None:return 0
if root.left == None and root.right == None:
return 1
lh = self.maxDepth(root.left)
rh = self.maxDepth(root.right)
return max(rh,lh) + 1
24-107、二叉树的层次遍历
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其自底向上的层次遍历为:
[ [15,7], [9,20], [3] ]
队列:
class Solution:
def levelOrderBottom(self, root):
queue = []
cur = [root] # 接下来要循环的当前层节点,存的是节点
while cur: # 当前层存在结点时
cur_layer_val = [] # 初始化当前层结果列表为空,存的是val
next_layer_node = [] # 初始化下一层结点列表为空
for node in cur: # 遍历当前层的每一个结点
if node: # 如果该结点不为空,则进行记录
cur_layer_val.append(node.val) # 将该结点的值加入当前层结果列表的末尾
next_layer_node.extend([node.left, node.right])
# 将该结点的左右孩子结点加入到下一层结点列表
if cur_layer_val: # 只要当前层结果列表不为空
queue.insert(0, cur_layer_val) # 则把当前层结果列表插入到队列首端
cur = next_layer_node # 下一层的结点变成当前层,接着循环
return queue # 返回结果队列
25-108、将有序数组转化为二叉搜索树
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定有序数组: [-10,-3,0,5,9],
一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5
解析:
平衡二叉搜索树需要保证俩点:
根节点大于左子树任意节点,小于右子树任意节点
左右子数高度相差不超过 1
由以上性质,一个可行的递归条件可以得出:
每次返回的根节点处于数组中间,以其左右半数组分别递归构造左右子树
那么就意味着左子小于根,右子大于根,且所有节点左右子树节点数相差不超过 1 (由于递归的构树方式相同,所有节 点都满足高度平衡)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> TreeNode:
if nums:
m = len(nums) // 2
r = TreeNode(nums[m])
r.left, r.right = map(self.sortedArrayToBST, [nums[:m], nums[m+1:]])
return r
26-110、平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
示例 1:
给定二叉树 [3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
返回 true 。
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
if not root:
return True
return abs(self.height(root.right)-self.height(root.left))<2 and self.isBalanced(root.left) and self.isBalanced(root.right)
# 求高度
def height(self, node):
if not node:
return 0
return 1+max(self.height(node.right),self.height(node.left))
27-111、平衡二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最小深度 2.
这道题和maximum depth题正好相反,是求根节点到叶子节点的最小深度,为确保统计的是根节点到叶子节点的深度,需要注意:
当前节点左右子树有一个为空时,返回的应是非空子树的最小深度,而不是空子树深度0;若返回0相当于把当前节点认为成叶子节点,与此节点有非空子树矛盾。
当左右子树都不为空时,和maximum depth题一样,返回左右子树深度的最小值。
当左右子树都为空时,只有1个根节点深度为1(根节点与叶子节点重合)。
class Solution:
def minDepth(self, root: TreeNode) -> int:
if not root: return 0
if not root.left: return self.minDepth(root.right) + 1
if not root.right: return self.minDepth(root.left) + 1
return min(self.minDepth(root.left), self.minDepth(root.right)) + 1
28-112、路径总和
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例:
给定如下二叉树,以及目标和 sum = 22,
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
递归:
def hasPathSum(self, root: TreeNode, sum: int) -> bool:
if not root: return False
if not root.left and not root.right and sum - root.val == 0:return True
return self.hasPathSum(root.left, sum - root.val) or self.hasPathSum(root.right, sum - root.val)
29-118、杨辉三角
给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
示例:
输入: 5
输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
可以画一个表格数组
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
if numRows <= 0: return []
res = []
for row in range(numRows):
if row == 0:
res.append([1])
else:
tmp = [1]
for c in range(row):
# 当c == row-1时, 表示当前列为倒数第一个元素, 该元素与哨兵位置的0元素相加.
sum_ = (res[row-1][c] + 0) if (c == row-1) else (res[row-1][c]+res[row-1][c+1])
tmp.append(sum_)
res.append(tmp)
return res
30-119、杨辉三角2
给定一个非负索引 k,其中 k ≤ 33,返回杨辉三角的第 k 行。
在杨辉三角中,每个数是它左上方和右上方的数的和。
示例:
输入: 3
输出: [1,3,3,1]
进阶:你可以优化你的算法到 O(k) 空间复杂度吗?
def getRow(rowIndex):
# j行的数据, 应该由j - 1行的数据计算出来.
# 假设j - 1行为[1,3,3,1], 那么我们前面插入一个0(j行的数据会比j-1行多一个),
# 然后执行相加[0+1,1+3,3+3,3+1,1] = [1,4,6,4,1], 最后一个1保留即可.
r = [1]
for i in range(1, rowIndex + 1):
r.insert(0, 0)
# 因为i行的数据长度为i+1, 所以j+1不会越界, 并且最后一个1不会被修改.
for j in range(i):
r[j] = r[j] + r[j + 1]
return r
31-121、买卖股票的最佳时机①-买卖一次
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
动态规划:最大价格=max(前一天的最大利润,今天价格-之前的最小价格)
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices) <= 1: return 0
min_price = prices[0]
max_p = 0
for i in range(len(prices)):
min_price = min(min_price,prices[i])
max_p = max(max_p,prices[i]-min_price)
return max_p
122、买卖股票的最佳时机②-买卖多次
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
等价于每天都买卖,即把正数差值相加即为最大利润:[-6,4,-2,3,-2],最大为4+3=7
-
复杂度分析:
- 时间复杂度 O(N)O(N) : 只需遍历一次
price
; - 空间复杂度 O(1)O(1) : 变量使用常数额外空间。
- 时间复杂度 O(N)O(N) : 只需遍历一次
class Solution:
def maxProfit(self, prices: List[int]) -> int:
max_p=0
for i in range(1,len(prices)):
tmp = prices[i]-prices[i-1]
if tmp > 0:
max_p += tmp
return max_p
123、买卖股票的最佳时机③--最多两笔交易
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
四种状态:buy1[i]表示前i天第一次交易买入后剩下最多的钱;shell1[i]表示前i天第一次交易卖出后剩下最多的钱;
buy2[i]表示前i天第二次交易买入后剩下最多的钱; shell2[i]表示前i天第二次交易卖出后剩下最多的钱;
则:buy1[i] = max{buy1[i-1],,-prices[i]}
shell[i]=max{shell1[i-1],buy1[i-1]+prices[i]}
buy2[i] = max{buy2[i-1],shell2[i-1] - prices[i]}
shell2[i] = max{shell2[i-1],buy2[i-1]+prices[i]}
from sys import maxsize
class Solution:
def maxProfit(self, prices: List[int]) -> int:
buy1,sell1,buy2,sell2 = -maxsize,0,-maxsize,0
for i in range(len(prices)):
buy1 = max(buy1,-prices[i])
sell1 = max(sell1,buy1+prices[i])
buy2 = max(buy2,sell2-prices[i])
sell2 = max(sell2,buy2+prices[i])
return sell2
188、买卖股票的最佳时机④--最多K次
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [2,4,1], k = 2
输出: 2
解释: 在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
解:由于188题有限制条件,即最多只能交易k次,还需要记录交易次数,还需要一维来记录交易了多少次
此时状态方程变成了
mp[i][h][k]
i 表示天数
j 表示是否持有股票
k 表示之前交易了多少次
此时动态规划的转移方程变成了如下所示:
// 为了方便理解 把k放到了第二维
//前一天 要么不操作 要么卖掉了一股
mp[i][k][0] = max(mp[i-1][k][0], mp[i-1][k-1][1] + a[i])
// 前一天 要么不操作 要么买入了一股
mp[i][k][1] = max(mp[i-1][k][1], mp[i-1][k][0] - a[i)
想要求出最大的收益 只需要找到
mp[n-1, {0...k},0]的最大值即可
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
if not prices or not k:
return 0
n = len(prices)
# 如果k大于数组长度的一半,则可以用贪心解决
if k > n//2:
return self.greedy(prices)
# 动态规划
dp = [[[0] * 2 for _ in range(k+1)] for _ in range(n)]
res = []
# 设置初始状态
for i in range(k+1):
dp[0][i][0], dp[0][i][1] = 0, -prices[0]
# 开始两层循环
for i in range(1,n):
for j in range(k+1):
if not j:
dp[i][j][0] = dp[i-1][j][0]
else:
dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j-1][1] + prices[i])
dp[i][j][1] = max(dp[i-1][j][1], dp[i-1][j][0] - prices[i])
# 找到最大值
for m in range(k+1):
print(dp[n-1][m][0])
res.append(dp[n-1][m][0])
return max(res)
def greedy(self, prices):
res = 0
for i in range(1,len(prices)):
if prices[i] > prices[i-1]:
res += prices[i] - prices[i-1]
return res
309、买卖股票最佳时期含冷冻期
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices:
return 0
# 每个元祖表示当前持有股票以及不持有股票时所能最大化的自己的余额
temp = (-prices[0], 0) # 对第一个股价,若持有时余额为负股票价,不持有时为0
if len(prices) == 1:
return max(temp)
dp = [temp]
# 持有时余额最大为前两天最低价买入的情况,不持有时为一直不买和一买二卖中的最大值
temp = (-min(prices[0], prices[1]), max(0, prices[1] - prices[0]))
dp.append(temp)
# 持有时考虑保持前一天的持有和前两天不持有现在买入时的情况,不持有时考虑保持前一天的不持有
# 和前一天持有现在卖出时的情况
for p in prices[2:]:
temp = (max(dp[1][0], dp[0][1] - p), max(dp[1][1], dp[1][0] + p))
dp[:] = [dp[1], temp]
return max(dp[1])
714、买卖股票最佳时期含手续费
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每次交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
示例 1:
输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
注意:0 < prices.length <= 50000. 0 < prices[i] < 50000. 0 <= fee < 50000.
方法:动态规划
我们维护两个变量 \mathrm{cash}cash 和 \mathrm{hold}hold,前者表示当我们不持有股票时的最大利润,后者表示当我们持有股票时的最大利润。
在第 ii 天时,我们需要根据第 i - 1i−1 天的状态来更新 \mathrm{cash}cash 和 \mathrm{hold}hold 的值。对于 \mathrm{cash}cash,我们可以保持不变,或者将手上的股票卖出,状态转移方程为
cash = max(cash, hold + prices[i] - fee)
对于 \mathrm{hold}hold,我们可以保持不变,或者买入这一天的股票,状态转移方程为
hold = max(hold, cash - prices[i])
在计算这两个状态转移方程时,我们可以不使用临时变量来存储第 i - 1i−1 天 \mathrm{cash}cash 和 \mathrm{hold}hold 的值,而是可以先计算 \mathrm{cash}cash 再计算 \mathrm{hold}hold,原因是在同一天卖出再买入(亏了一笔手续费)一定不会比不进行任何操作好。
复杂度分析
- 时间复杂度:O(n)O(n),其中 nn 是 \mathrm{prices}prices 数组的长度。
- 空间复杂度:O(1)O(1)。
class Solution(object):
def maxProfit(self, prices, fee):
cash, hold = 0, -prices[0]
for i in range(1, len(prices)):
cash = max(cash, hold + prices[i] - fee)
hold = max(hold, cash - prices[i])
return cash