详解二叉树算法
binary tree 二叉树
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
# 数组转化为二叉树,可以包含空值但必须空值的子节点也有空值填充
def list2tree(nums, idx):
if idx > len(nums) - 1:
return
tree = TreeNode(nums[idx])
tree.left = list2tree(nums, idx * 2 + 1)
tree.right = list2tree(nums, idx * 2 + 2)
return tree
核心知识点:dfs & bfs
二叉树的 dfs(depth first search) 分为前序、中序和后序遍历,区分的标准是按根节点访问顺序。如:
- 前序遍历 preorder traversal: 根-左树前序遍历的结果-右树前序遍历的结果
- 中序遍历 inorder traversal: 左树中序遍历的结果-根-右树中序遍历的结果
- 后序遍历 postorder traversal: 左树后序遍历的结果-右树后序遍历的结果-根
二叉树的 bfs(breadth first search) 即为层次遍历,bfs 通常用一个队列来暂存树的节点,这种思路常常被考察。
下面以 [1,2,3,4,5,6,7] 组成的树来举例
复杂度分析
时间复杂度:O(N)
每个节点都会遍历一次
空间复杂度:O(N)
为递归过程中栈的开销,平均情况下为 O(logN)
,最坏情况下树呈现链状,为 O(N)
。
# 前序遍历:1 2 4 5 3 6 7
def pre_order(tree):
res = []
def dfs(tree):
if tree is None:
return
else:
res.append(tree.val)
dfs(tree.left)
dfs(tree.right)
dfs(tree)
return res
# 中序遍历:4 2 5 1 6 3 7
def in_order(tree):
res = []
def dfs(tree):
if tree is None:
return
else:
dfs(tree.left)
res.append(tree.val)
dfs(tree.right)
dfs(tree)
return res
# 后序遍历:4 5 2 6 7 3 1
def post_order(tree):
res = []
def dfs(tree):
if tree is None:
return
else:
dfs(tree.left)
dfs(tree.right)
res.append(tree.val)
dfs(tree)
return res
# 层次遍历:1 2 3 4 5 6 7
def bfs(tree):
res, tmp = [], []
if tree is None:
return
tmp.append(tree)
while len(tmp) > 0:
first = tmp.pop(0)
res.append(first.val)
if first.left is not None:
tmp.append(first.left)
if first.right is not None:
tmp.append(first.right)
return res
二叉树的反序列化(构建二叉树)
==注:==实践证明使用 dfs 序列化出来的二叉树并不一定符合我们的设定,以 [1,2,3] 为例 dfs 反序列化构成的二叉树会是一个链,所以我们反序列化二叉树优先推荐 bfs
实践证明使用 dfs 序列化出来的二叉树并不一定符合我们的设定
以 [1,2,3] 为例构成的二叉树会是一个链
class ConvertBFS:
def deserialize(self, data: List[Optional[int]]) -> Optional[TreeNode]:
"""
bfs 反序列化,将数组转化成二叉树
:param data:
:return:
"""
queue = []
if data is None:
return None
# data[0] 肯定不是空,因为没必要
tree = TreeNode(data[0])
queue.append(tree)
i = 1
while len(queue) > 0 and i < len(data):
tmp = queue.pop(0)
if data[i] is not None:
tmp.left = TreeNode(data[i])
queue.append(tmp.left)
i += 1
if i < len(data) and data[i] is not None:
tmp.right = TreeNode(data[i])
queue.append(tmp.right)
i += 1
return tree
class ConvertDFS:
"""
没法根据中序遍历的结果来重建二叉树,因为根据中序遍历构建的二叉树并不唯一
"""
def serialize_pre(self, root: Optional[TreeNode]) -> List[Optional[int]]:
def dfs(root: Optional[TreeNode], res: List[Optional[int]]):
if root is None:
res.append(None)
return
else:
res.append(root.val)
dfs(root.left, res)
dfs(root.right, res)
return
res = []
dfs(root, res)
return res
def deserialize_pre(self, data: List[Optional[int]]) -> Optional[TreeNode]:
"""
根据前序遍历的结果还原一个二叉树
:param data:
:return:
"""
if data is None:
return None
tmp = data.pop(0)
if tmp is not None:
tree = TreeNode(tmp)
tree.left = self.deserialize_pre(data)
tree.right = self.deserialize_pre(data)
return tree
else:
return None
def serialize_post(self, root: Optional[TreeNode]) -> List[Optional[int]]:
res = []
def dfs(root: Optional[TreeNode], res: List[Optional[int]]):
if root is None:
res.append(None)
return
dfs(root.left, res)
dfs(root.right, res)
res.append(root.val)
return
dfs(root, res)
return res
def deserialize_post(self, data: List[Optional[int]]) -> Optional[TreeNode]:
if not data:
return None
tmp = data.pop()
if tmp:
tree = TreeNode(tmp)
tree.right = self.deserialize_post(data)
tree.left = self.deserialize_post(data)
return tree
else:
return None
常见考察题型
是否为对称二叉树-leetcode-101
将题目转化成总是两棵树做对比就清晰了,其实我们只需要对比是不是空树,以及根节点的值是否对,其他的扔到下次循环来做就可以。使用队列的方式和 btree 的 bfs 十分相似。另一种方式是使用递归,两者复杂度均为:(n 为树的节点数)
时间复杂度:O(n)
空间复杂度:O(n) 注:这里的空间复杂度和递归使用的栈空间有关,这里递归层数不超过 n。
class Solution:
def isSymmetric_script(self, root: Optional[TreeNode]) -> bool:
# 首先使用 inorder dfs,得到一个数组,然后判断数组中的元素是否是对称的,这种方法属于取巧方法,对于 [1, 2, 2, 2, None, 2] 这种场景就不适用了
# 🤔这个题如果能把 null 换成一个其他字母来标识就好了[1, 2, 2, 2, "x", 2]
def inorder_dfs(tree, res):
if tree is None:
return
if tree.left is not None:
inorder_dfs(tree.left, res)
res.append(tree.val)
if tree.right is not None:
inorder_dfs(tree.right, res)
res = []
inorder_dfs(root, res)
length = len(res)
print(res)
if length % 2 == 0:
return False
for i in range(int(length / 2) + 1, length):
if res[i] != res[length - 1 - i]:
return False
return True
def isSymmetric_0(self, root: Optional[TreeNode]) -> bool:
if root is None:
return False
# 通过对左右树进行对比得到是不是相同的结论
def check(left, right):
if left is None and right is None:
return True
if left is None or right is None:
return False
return left.val == right.val and check(left.left, right.right) and check(left.right, right.left)
return check(root.left, root.right)
def isSymmetric_1(self, root: Optional[TreeNode]) -> bool:
if root is None:
return False
queue = []
# use a queue like bfs
queue.append(root.left)
queue.append(root.right)
while len(queue) > 0:
left = queue.pop(0)
right = queue.pop(0)
if left is None and right is None:
continue
if left is None or right is None:
return False
if left.val != right.val:
return False
queue.append(left.left)
queue.append(right.right)
queue.append(left.right)
queue.append(right.left)
return True
二叉树的最小深度-leetcode-111
解题思路
注意链状🌲的深度即为树的高度
dfs 获取二叉树的最小深度
时间复杂度:O(N)
每个节点都会遍历到
空间复杂度:O(N)
平均为 O(logN)
当树为链状的时候即为 O(N)
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
# 注意链状🌲的深度即为树的高度
# 时间复杂度:O(N) 每个节点都会遍历到
# 空间复杂度:O(N) 平均为 O(logN) 当树为链状的时候即为 O(N)
if root is None:
return 0
if root.left is None and root.right is None:
return 1
if root.left is None:
return 1 + self.minDepth(root.right)
elif root.right is None:
return 1 + self.minDepth(root.left)
return 1 + min(self.minDepth(root.left), self.minDepth(root.right))
二叉树的最大深度-leetcode-104
class Solution:
# 使用 bfs 来获取树的最大深度
# 时间复杂度 O(N) 因为每个节点入队列一次
# 空间复杂度 O(N) 队列的最大长度是节点数量
def maxDepth0(self, root: Optional[TreeNode]) -> int:
queue, res = [], 0
if root is None:
return res
else:
queue.append(root)
while len(queue) > 0:
res += 1
length = len(queue)
for i in range(length):
first = queue.pop(0)
if first.left is not None:
queue.append(first.left)
if first.right is not None:
queue.append(first.right)
return res
# 使用 dfs 来获取最大深度
# 时间复杂度:O(n),其中 n 为二叉树节点的个数。每个节点在递归中只被遍历一次。
# 空间复杂度:O(height),其中 height 表示二叉树的高度。
# 递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
def maxDepth1(self, root: Optional[TreeNode]) -> int:
if root is None:
return 0
return max(self.maxDepth1(root.left), self.maxDepth1(root.right)) + 1
由中序遍历和前序/后序遍历重建二叉树-leetcode-105-106
这个题的思路比较统一,都是通过中序遍历确定出根节点的位置,从而知道左树大小和右树大小
class Solution:
# 根据中序遍历和前序遍历构建树
# time complexity:O(N),其中 N 是树中的节点个数。
# space complexity:O(N),除去返回的答案需要的 O(N) 空间之外,我们还需要使用 O(N) 的空间存储哈希映射,
# 以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h < N,所以总空间复杂度为 O(N)。
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
# 使用递归的方式来一直构建树,主体思路是建根 -> 找根
# 根-左树前序遍历的结果-右树前序遍历的结果
# 左树中序遍历的结果-根-右树中序遍历的结果
def myBuildTree(pre_start: int, pre_end: int, in_start: int, in_end: int):
# 这里是关键,起点大于终点的时候就得返回空
if pre_start > pre_end:
return None
pre_root = pre_start
# 根节点在中序遍历中的位置
in_root = index[preorder[pre_root]]
root = TreeNode(preorder[pre_root])
# 得到左子树中的节点数目
# [3,9,20,15,7] [9,3,15,20,7]
# 可见左子树的元素个数就只有 1 个
size_left_subtree = in_root - in_start
root.left = myBuildTree(pre_start + 1, pre_start + size_left_subtree, in_start, in_root - 1)
root.right = myBuildTree(pre_start + size_left_subtree + 1, pre_end, in_root + 1, in_end)
return root
n = len(preorder)
# 构造哈希映射,帮助我们快速定位某个值在中序遍历所在的位置。因为题目中已经给出限制元素都是惟一的
index = {element: idx for idx, element in enumerate(inorder)}
return myBuildTree(0, n - 1, 0, n - 1)
# 根据中序遍历和后序遍历构建树
# time complexity: O(N),树的每个节点在递归中都会被遍历一次
# space complexity: O(N), 返回结果需要 O(N) 的空间,哈希表暂存中序遍历的值也需要 O(N) 空间,递归时栈空间 O(h),取较大值 O(N)
# 递归时栈空间即为递归时调用栈占用的空间
def buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
index = {val: idx for idx, val in enumerate(inorder)}
def build_tree(in_start: int, in_end: int, post_start: int, post_end: int) -> Optional[TreeNode]:
if in_start > in_end:
return None
# 后续遍历的最后一个值一定是根节点
root_val = postorder[post_end]
# 获取根节点在中序遍历中的位置
root_idx = index[root_val]
# 获取左树节点数、右树节点数
left_count, right_count = root_idx - in_start, in_end - root_idx
tree = TreeNode(root_val)
tree.right = build_tree(root_idx+1, in_end, post_end - right_count, post_end-1)
tree.left = build_tree(in_start, root_idx-1, post_start, post_end-right_count-1)
return tree
length = len(inorder)
return build_tree(0, length-1, 0, length-1)
Path sumI-112-路径总和
解题思路
dfs 递归
时间复杂度:O(N)
N 是树的节点数,每个节点都会遍历到
空间复杂度:即递归调用栈的大小,平均情况下为 O(logN) 即二叉树的高度,但最坏的情况下是树呈链状即为 O(N)
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
# 使用 dfs 递归
# 时间复杂度:O(N) N 是树的节点数,每个节点都会遍历到
# 空间复杂度:即递归调用栈的大小,平均情况下为 O(logN) 即二叉树的高度,但最坏的情况下是树呈链状即为 O(N)
if root is None:
return False
if root.val == targetSum and root.left is None and root.right is None:
return True
return self.hasPathSum(root.left, targetSum - root.val) or self.hasPathSum(root.right, targetSum - root.val)
Path sumII-113-路径总和
解题思路
思路:减去根节点,递归拼接左右子树的结果
递归停止条件:root 为 None
时间复杂度:方案为深度优先搜索即 dfs。题解时间复杂度 O(N^2)
已知如果树是一个完全二叉树,那么叶子节点数不会超过 N/2 即路径总数不超过 N/2,同时树的深度不会超过 logN 即路径的元素个数不超过 logN,故若所有路径均符合条件时渐进时间复杂度为 O(NlogN)
. 但若一个树上半部分呈链状,下半部分为完全二叉树,那么树的深度则几乎可以认为是 N,因此最差情况下时间复杂度为 O(N^2)
空间复杂度:栈中元素不会超过节点的个数,故为 O(N)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
# 思路:减去根节点,递归拼接左右子树的结果
# 递归停止条件:root 为 None 或者 targetSum 比 root 小
res = []
if root is None:
return res
if targetSum == root.val and root.left is None and root.right is None:
res.append([root.val])
return res
else:
left_res = self.pathSum(root.left, targetSum - root.val)
right_res = self.pathSum(root.right, targetSum - root.val)
left_res.extend(right_res)
for item in left_res:
tmp = [root.val]
tmp.extend(item)
res.append(tmp)
return res
Path sumIII-437-路径总和
解题思路
由于要求起始节点可以不是根节点,结束节点也可以不是根节点,所以给定一个树,我们有两种答案,包含根节点和不包含根节点
time complexity: 深度优先搜索(dfs), 对于二叉树的每个节点求从该节点开始能够达到 target 的路径的数目时需要遍历以该节点为根节点的子树
的所有节点,因此求路径花费的最大时间复杂度为 O(N),每个节点都会作为根节点求一次,所以总的时间复杂度为 O(N^2)
space complexity: 递归调用栈占用空间大约为 O(N)
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
# 由于要求起始节点可以不是根节点,结束节点也可以不是根节点,所以给定一个树,我们有两种答案,包含根节点和不包含根节点
def dfs(tree: Optional[TreeNode], target: int, include: bool) -> int:
if tree is None:
return 0
if include is True:
tmp = 0
if tree.val == target:
tmp = 1
return tmp + dfs(tree.left, target - tree.val, True) + dfs(tree.right, target - tree.val, True)
else:
return dfs(tree.left, target, True) + dfs(tree.right, target, True) + dfs(tree.left, target, False) + \
dfs(tree.right, target, False)
return dfs(root, targetSum, True) + dfs(root, targetSum, False)
binary search tree 二叉搜索树
References:
详解二叉排序树(二叉搜索树、二叉查找树)以及Python实现相关操作🌟🌟🌟
数据结构(二):二叉搜索树(Binary Search Tree)🌟🌟🌟
概念
二叉搜索树或是空树或是满足如下性质的二叉树:
- 若左子树非空,则左子树上的所有节点的值小于根节点的值
- 若右子树非空,则右子树上所有节点的值均大于等于根节点的值
- 左右子树本身各为二叉搜索树
性质或特点
中序遍历的结果是一个递增的有序序列
适用场景
二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低,最佳情况下为 Olog(N)
。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等,对于可能形成偏斜二叉树的问题可以经由树高改良后的平衡树将搜索、插入、删除的时间复杂度都维持在Olog(N)
,如AVL树、红黑树等。
复杂度分析
查询:查询节点过程是,比较元素值是否相等,相等则返回,不相等则判断大小情况,递归/迭代查询左、右子树,直到找到相等的元素,或子节点为空,返回节点不存在
查询时间复杂度:O(logN)~O(N)
插入:插入节点的过程中,比较元素值是否相等,值相等标识已经存在则返回,不相等则判断大小情况,递归/迭代查询左、右子树,直到找到相等的元素,或子节点为空,则将节点插入该空节点位置。
插入时间复杂度:O(logN)~O(N)
# bst 的搜索,递归版
# 时间复杂度:O(logN)~O(N) 最差是树呈现链状的时候
# 空间复杂度:O(logN)~O(N)
def bst_search_recursive(tree: Optional[TreeNode], val: int) -> Optional[TreeNode]:
if tree is None:
return None
if tree.val == val:
return tree
elif val < tree.val:
return bst_search_recursive(tree.left, val)
else:
return bst_search_recursive(tree.right, val)
# bst 的搜索,迭代版
# # 时间复杂度:O(logN)~O(N) 最差是树呈现链状的时候
# # 空间复杂度:O(1)
def bst_search_iteration(tree: Optional[TreeNode], val: int) -> bool:
if tree is None:
return False
if tree.val == val:
return True
while tree is not None and tree.val != val:
if val < tree.val:
tree = tree.left
else:
tree = tree.right
if tree is None:
return False
return True
# 构造 bst
# 插入值-递归
# 查询节点过程是,比较元素值是否相等,相等则返回,不相等则判断大小情况,迭代查询左、右子树,直到找到相等的元素,或子节点为空,返回节点不存在
# 插入节点的过程是,比较元素值是否相等,相等则返回,表示已存在,不相等则判断大小情况,迭代查询左、右子树,直到找到相等的元素,或子节点为空,则将节点插入该空节点位置。
# 时间复杂度:O(logN)~O(N) 最差是树呈现链状的时候
# 空间复杂度:O(logN)~O(N) 为递归调用栈的开销
def bst_insert_recursive(tree: Optional[TreeNode], val: int):
if tree is None:
tree = TreeNode(val)
return tree
if tree.val == val:
print("existed!")
return
elif val < tree.val:
if tree.left is None:
tree.left = TreeNode(val)
return tree
bst_insert_recursive(tree.left, val)
else:
if tree.right is None:
tree.right = TreeNode(val)
return tree
bst_insert_recursive(tree.right, val)
return tree
# 时间复杂度 O(logN)~O(N)
# 空间复杂度 O(logN)~O(N) 为队列占用的空间
def bst_insert_iteration(tree: Optional[TreeNode], val: int):
node = TreeNode(val)
if tree is None:
tree = node
return tree
queue = [tree]
while len(queue) > 0:
tmp_node = queue.pop(0)
if val < tmp_node.val:
if tmp_node.left is None:
tmp_node.left = node
return tree
else:
queue.append(tmp_node.left)
else:
if tmp_node.right is None:
tmp_node.right = node
return tree
else:
queue.append(tmp_node.right)
return tree
def build_bst(nums: List[int]) -> Optional[TreeNode]:
length = len(nums)
tree = None
for i in range(length):
# tree = bst_insert_recursive(tree, nums[i])
tree = bst_insert_iteration(tree, nums[i])
return tree
BST 的反序列化 🌟🌟🌟🌟🌟
区别于普通二叉树的序列化需要加入 None 来保证能够根据序列化结果唯一确认一个二叉树,bst 可以在序列化时忽略为空的节点,然后根据序列化的结果能唯一确定一棵 bst
class Codec:
def serialize(self, root: Optional[TreeNode]) -> str:
"""Encodes a tree to a single string.
bst 和 t 最大的区别是中序遍历的结果是递增的,所以遇到空值我们不追加到字符串中也能确定原序列
同时题目要求 The encoded string should be as compact as possible.
"""
res = []
def dfs(root: Optional[TreeNode], res: List[Optional[int]]):
if root is None:
return
dfs(root.left, res)
dfs(root.right, res)
res.append(root.val)
return
dfs(root, res)
return ",".join(map(str, res))
def deserialize_0(self, data: str) -> Optional[TreeNode]:
"""
根据后序遍历的结果,同时知道 tree 是 bst, 可以获得中序遍历,根据后序遍历和中序遍历结果得到原 tree
:param data:
:return:
"""
if not data:
return None
data0 = data.split(',')
post = list(map(int, data0))
mid = list(map(int, data0))
mid.sort()
table = {}
for i in range(len(mid)):
table[mid[i]] = i
def build_tree(post_start: int, post_end: int, mid_start: int, mid_end: int) -> Optional[TreeNode]:
if post_end >= len(post):
return None
root = TreeNode(str(post[post_end]))
mid_idx = table[post[post_end]]
left_len = mid_idx - mid_start
right_len = mid_end - mid_idx
if left_len > 0:
root.left = build_tree(post_start, post_start + left_len - 1, mid_start, mid_idx - 1)
if right_len > 0:
root.right = build_tree(post_end - right_len, post_end-1, mid_idx + 1, mid_end)
return root
return build_tree(0, len(post) - 1, 0, len(mid) - 1)
def deserialize(self, data: str) -> TreeNode:
"""
根据 bst 的性质来解决问题,此时序列化的结果里不应该包含 None,应该尽量紧凑
时间复杂度:O(N)
空间复杂度:O(N)
:param data:
:return:
"""
arr = list(map(int, data.split(',')))
def construct(lower: int, upper: int) -> TreeNode:
# 不符合条件的不必弹出栈
if arr == [] or arr[-1] < lower or arr[-1] > upper:
return None
val = arr.pop()
root = TreeNode(str(val))
root.right = construct(val, upper)
root.left = construct(lower, val)
return root
return construct(float('-inf'), float('inf'))
同样,可以根据前序遍历的结果重新构建一棵 bst
class Solution:
def serialize(self, root: Optional[TreeNode]) -> str:
res = []
def dfs(root: Optional[TreeNode], res: List[Optional[int]]):
if root is None:
return
res.append(root.val)
dfs(root.left, res)
dfs(root.right, res)
return
dfs(root, res)
return ",".join(map(str, res))
def deserialize_0(self, preorder: List[int]) -> Optional[TreeNode]:
"""
已知树的 preorder 同时知道树是 bst,所以知道了 inorder
解法一:变成根据前序遍历和中序遍历结果重建树
时间复杂度:O(NlogN) 构建树是 O(N) 排序是 O(NlogN)
空间复杂度:O(N) 当树呈现链状时,空间复杂度最高为 O(N)
:param preorder:
:return:
"""
import copy
inorder = copy.deepcopy(preorder)
inorder.sort()
tb = {}
for i in range(len(inorder)):
tb[inorder[i]] = i
# 根 左 右
def build_tree(pre_start: int, pre_end: int, in_start: int, in_end: int) -> Optional[TreeNode]:
if pre_start > pre_end or in_start > in_end:
return None
root = TreeNode(preorder[pre_start])
root_idx = tb[preorder[pre_start]]
left_len = root_idx - in_start
root.left = build_tree(pre_start + 1, pre_start + left_len, in_start, root_idx - 1)
root.right = build_tree(pre_start + left_len + 1, pre_end, root_idx + 1, in_end)
return root
return build_tree(0, len(preorder) - 1, 0, len(preorder) - 1)
def deserialize(self, preorder: List[int]) -> Optional[TreeNode]:
"""
时间复杂度:O(N)
空间复杂度:O(N)
:param preorder:
:return:
"""
def dfs(lower: int, upper: int) -> Optional[TreeNode]:
if len(preorder) <= 0 or preorder[0] < lower or preorder[0] > upper:
return None
root = TreeNode(preorder.pop(0))
root.left = dfs(lower, root.val)
root.right = dfs(root.val, upper)
return root
return dfs(float('-inf'), float('inf'))
常见题型
是否是 bst-leetcode-98
方法 1:使用 bst 中序遍历是递增序列的特点
方法 2:我们已经知道 bst 根节点永远比左树大、永远比右树小。且左树、右树都符合此特点。如果我们只按根节点来判断,会失去方向,因为在递归的过程中会发现不知道当前的树是最初的左树还是右树了,此时必须加入一个参数,但实验下来不能达到预期。所以不妨总是判断一个节点是不是比最小值大,比最大值小,这样就能保证进行到叶子节点时控制它的大小。具体参考下文代码:
class Solution:
# 方法 1:使用 bst 中序遍历是递增序列的特点。使用类属性
# time complexity: O(N) 每个节点都遍历了一次
# space complexity: O(N) 递归时调用栈的开销,如果是链状,最大开销就是 O(N)
last_val = -sys.maxsize - 1
res = True
def isValidBST0(self, root: Optional[TreeNode]) -> bool:
if root is None:
return True
def in_order(tree: Optional[TreeNode]) -> bool:
if tree is None:
return
in_order(tree.left)
if tree.val <= Solution.last_val:
Solution.res = False
Solution.last_val = tree.val
in_order(tree.right)
in_order(root)
return Solution.res
# 判断是不是 bst 其实很简单,只需要判断根节点是不是比左树大,是不是比右树小
# 递归的检查左树是不是 bst,且左树所有节点必须小于根节点
# 递归的检查右树是不是 bst,且右树所有节点必须大于根节点
# 方法 2:递归判断如上论述
# time complexity: O(N) 每个节点都遍历了一次
# space complexity: O(N) 递归时调用栈的开销,如果是链状,最大开销就是 O(N)
def isValidBST(self, root: Optional[TreeNode]) -> bool:
def recursion(tree: Optional[TreeNode], low: int, high: int) -> bool:
# 如何解决左树必须比根节点小,右树必须比根节点大
# 题解给了我们一种很好的方法就是限制住区间,因为左树永远比任何根节点小,右树永远必须比任何根节点大,我们如果要区分比较大小就必须加一个参数
# 标识比较大还是比较小,与其这样不如使用区间的形式
if tree is None:
return True
val = tree.val
if val <= low or val >= high:
return False
if not recursion(tree.left, low, val):
return False
if not recursion(tree.right, val, high):
return False
return True
return recursion(root, -sys.maxsize - 1, sys.maxsize)
recover bst-leetcode-99
最初我的一个思路是按照 98 题判断是不是 bst 的方式,只不过把 min_val max_val 替换成树,遇到不符合要求的就交换两个节点,然而最终发现无法满足 [2,3,1]
这种场景。如果直接暴力检查根节点是不是比左树大,比右树小就无法满足:[3, 1, 4, None, None, 2]
这个场景。
参考题目答案:我们知道 bst 中序遍历的结果是递增序列,与此同时只有一对元素交换了位置:1 2 3 4 5 6 如果两个元素交换位置比如 1 和 6 交换后 6 2 3 4 5 1 就会出现两个位置不符合递增状态即 2 和 1 所在的位置
如果是相邻的元素交换 1 3 2 4 5 6 则 2 所在位置有问题
如果 1 4 3 2 5 6 则 3 2 所在位置有问题
发现一个规律:
- 如果不是相邻的元素换了,如果我们按递增的规律去查找会发现第一个结果位置往前一位是对的,第二个结果位置是对的,交换两个对的位置即可
- 如果是相邻的元素换了,那么结果位置和结果位置前一位交换即可
class Solution:
def recoverTree(self, root: Optional[TreeNode]) -> None:
"""
Do not return anything, modify root in-place instead.
"""
def exchange(tree1: Optional[TreeNode], tree2: Optional[TreeNode]):
tmp_val = tree1.val
tree1.val = tree2.val
tree2.val = tmp_val
res, mark = [], []
def in_order(tree: Optional[TreeNode]):
if tree is None:
return
in_order(tree.left)
if len(res) > 0:
last = res[-1]
if last.val > tree.val:
mark.append(tree)
res.append(tree)
in_order(tree.right)
in_order(root)
res_hash = {val: idx for (idx, val) in enumerate(res)}
if len(mark) == 1:
pos = res_hash[mark[0]]
exchange(res[pos], res[pos - 1])
if len(mark) == 2:
pos1 = res_hash[mark[0]] - 1
pos2 = res_hash[mark[1]]
exchange(res[pos1], res[pos2])
find bst lowest common ancestor-235
做题时如果点明树是 bst,那么就需要考虑到 bst 的典型特征。
方法一:寻找两个节点各自走过的路径集合,相当于遍历两次树
方法二:遍历一次树,是结合 bst 比较巧妙的解法
class Solution:
# time complexity: O(N) 最坏的情况下树呈现链状,则时间复杂度最差为 O(N)
# space complexity: O(N) 最坏的情况下需要的空间
def lowestCommonAncestor0(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
# 方法 2, 将寻找 p q 节点的路径收集起来,对比到最后一个相同的节点即为 lowest common ancestor
def find_route(root: TreeNode, tree: TreeNode) -> List[TreeNode]:
res = [root]
while True:
if tree.val == root.val:
return res
elif tree.val > root.val:
root = root.right
res.append(root)
else:
root = root.left
res.append(root)
return res
p_route = find_route(root, p)
q_route = find_route(root, q)
length, i, result = min(len(p_route), len(q_route)), 0, None
while i < length:
if p_route[i].val == q_route[i].val:
result = p_route[i]
i += 1
else:
break
return result
# time complexity: O(N) N 为
# space complexity: O(1)
def lowestCommonAncestor1(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
# 需要结合 bst 的性质
ancestor = root
while True:
if p.val < ancestor.val and q.val < ancestor.val:
ancestor = ancestor.left
elif p.val > ancestor.val and q.val > ancestor.val:
ancestor = ancestor.right
# 针对剩余场景:如果值分布在根两侧,如果其中一个值就是根
else:
break
return ancestor
AVL(Adelson-Velsky Landis tree)平衡二叉搜索树
概念
自平衡二叉查找树。在 AVL 树中任何节点的两个子树的高度最大差别为 1,所以它也被称为高度平衡树。
Balance Factor 平衡因子
将二叉树上节点的左子树高度减去右子树高度的值称为该节点的平衡因子 BF(Balance Factor)。AVL 树的 BF 的取值范围为 [-1,1]。如果发现某个节点的 BF 值不在此范围,则需要对树进行调整。
性质或特点
-
本身首先是一棵二叉搜索树。
-
带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。