二叉树BinaryTree
总体框架:
- 二叉树算法设计的框架:关注当前节点要做什么,左右子树交给递归框架,不用当前节点操心
- 一旦涉及建树、修改等操作,函数最终应该返回TreeNode节点,并且接收递归调用的返回值(赋值给节点的左右子树)
递归问题,如何写函数
二叉树的问题多为递归问题,任何递归型的问题弄清“灵魂三问”:
1. 这个函数是干什么的?
2. 函数参数是什么?
3. 得到函数的递归结果,应该干什么?
实际上,动态规划中要明确函数的“定义”、“状态”、“选择”,其本质就是上面的“灵魂三问”
可见,二叉树、递归与动态规划,内在套路是有一致性的、是相通的
完全二叉树、满二叉树
求节点个数:
- 对于一般二叉树,遍历所有节点
- 对于满二叉树,求出深度d,节点总数为2^d-1
- 对于完全二叉树,分别处理左右子树结点数(其两棵子树中至少有一棵是满二叉树)
利用特性:对于迭代中的每棵子树,向左走到底得到深度dl、向右走到底得到深度dr,如果dl==dr,该子树为完全二叉树,按照技巧2处理,否则按照一般二叉树的方式处理
遍历二叉树
遍历二叉树的方式有两种:递归遍历和迭代(层次)遍历
1. 递归遍历(DFS),即前/中/后序遍历
- 应用:求二叉树的深度/结点总数/结点值的总和
前序遍历,处理顺序大致是“从上往下”;后序遍历,大致是“从下往上”
框架:
class TreeNode(object):
"""二叉树的结点"""
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def traverse(root: TreeNode):
"""二叉树的递归遍历"""
# 在此前序遍历
traverse(root.left)
# 在此中序遍历
traverse(root.right)
# 在此后序遍历
2. 迭代遍历(BFS,用队列实现),即层次遍历
- 应用:求二叉树的宽度、逐行打印二叉树
框架:
from queue import Queue
def traverse(root: TreeNode):
"""二叉树的递归遍历"""
if root is None:
return
q = Queue()
q.put(root)
while not q.empty():
node = q.get()
# 层次遍历代码,在此处访问节点
# 每取出一个节点,将其左右子节点入队
if node.left:
q.put(node.left)
if node.right:
q.put(node.right)
具体实现在下面“序列化和反序列化二叉树”部分中介绍
序列化和反序列化二叉树
序列化
- 序列化理解为二叉树的前/中/后序遍历的结果
- 因此序列化的本质就是遍历二叉树
反序列化
-
反序列化理解为根据遍历结果还原二叉树
核心是:在遍历结果中确定根、左右子树的下标范围,然后建立根、递归建立其左右子树 -
算法框架:大多情况下反序列化的算法框架与序列化基本相同,即遍历二叉树的框架
前序遍历实现
序列化:就是前序遍历,在遍历同时注意打印值即可(None打印为#)
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Codec:
def serialize(self, root):
"""前序遍历的序列化
在遍历同时注意打印值即可(None打印为#)"""
ans = []
def traverse(node):
nonlocal ans
if node is None:
ans.append('#')
return
# 前序遍历
ans.append(str(node.val))
traverse(node.left)
traverse(node.right)
traverse(root)
return ','.join(ans)
反序列化:遵循前序遍历的规则,先确定根,然后递归生成左右子树
class Codec:
def deserialize(self, data):
"""前序遍历的反序列化
整体框架同序列化"""
nodes = data.split(',') # 节点的列表
cur = 0
length = len(nodes)
def buildTree():
"""使用[cur,末尾]范围构造子树"""
nonlocal cur, length
if cur >= length:
return None
# 遵循前序遍历的规则,先确定根,然后递归生成左右子树
first = nodes[cur] # 列表最左侧就是根节点
cur += 1 # 处理完一个节点
# 空节点,直接返回
if first == '#':
return None
# 非空节点,需要建立左右子树
root = TreeNode(int(first))
root.left = buildTree()
root.right = buildTree()
return root
return buildTree()
中/后序遍历实现
序列化:方式与前序遍历类似,改变打印节点值的位置即可
反序列化:
- 中序遍历的反序列化:单靠中序遍历无法反序列化(因为不知道根节点在中间的那个位置)
- 后序遍历的反序列化:
显然不能继续套用后序遍历框架,因为找不到左右子树的根,仍应先确定根,然后递归生成左右子树。应该从后往前处理,即(后序遍历结果)根在首字符,前一个字符就是右子树的根,(处理完右子树后)再前一个字符就是左子树的根
层次遍历实现
层次遍历需要用队列实现(BFS)
序列化:
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
from queue import Queue
class Codec:
def serialize(self, root):
"""中序遍历的序列化"""
if root is None:
return '#'
ans = [] # 保存序列化的结果
q = Queue()
q.put(root)
while not q.empty():
node = q.get()
if node is None:
ans.append('#')
continue
ans.append(str(node.val))
q.put(node.left)
q.put(node.right)
return ','.join(ans)
反序列化:
维护队列,只有非空节点入队
维护独立的指针i
,i
依次指向[中序遍历结果序列]中的每个节点,
每次取出队首节点,指针i
同步处理序列中的两个节点,分别作为队首父节点的左右儿子,得到左右儿子后继续将他们入队(符合层次遍历顺序)
from queue import Queue
class Codec:
def deserialize(self, data):
"""层次遍历的反序列化
同样用队列实现(队列中保存非空节点),
每次从队首取出父节点,同时指针i依次遍历节点序列,作为父节点的左右儿子,并入队
"""
nodes = data.split(',') # 节点的列表
if nodes[0] == '#':
return None
# 根节点
root = TreeNode(nodes[0])
q = Queue()
q.put(root) # 根节点入队
i = 1
length = len(nodes)
while i < length: # 迭代,每次从队列中取出一个节点,左右儿子根据指针i获得
# 取出父节点
father = q.get()
nextVal = nodes[i] # 下一个节点的值,若为#则代表空节点
i += 1 # 后移指针
# 左儿子
if nextVal != '#':
father.left = TreeNode(int(nextVal))
q.put(father.left)
else:
father.left = None
nextVal = nodes[i] # 下一个节点的值,若为#则代表空节点
i += 1 # 后移指针
# 右儿子
if nextVal != '#':
father.right = TreeNode(int(nextVal))
q.put(father.right)
else:
father.right = None
return root
最近公共祖先LCA
求二叉搜索树的最近公共祖先
从根节点开始,利用BST的特性,根据当前节点值和p、q节点值判断:
- 如果两个节点都在当前节点的左/右子树,则进入左/右子树
- 否则,两节点一定不在同一子树,当前节点就是“分叉点”,返回当前结点
求二叉树的最近公共祖先
普通的二叉树,无法像BST那样直接判断某节点是否在子树中
- 首先,二叉树的问题,先把遍历框架写出来准没错
- 类似于上面的思路,站在当前的节点上,我们需要看其左/右子树的情况,若当前根节点就是p/q,或若p、q节点分别在其左/右子树中,则当前节点就是公共祖先
- 更进一步,如何保证是最近公共祖先?从树的叶子节点往上走,第一个符合上述条件的节点,就是最近公共祖先,i.e.应采用后序遍历
- 当前的根节点是最近公共祖先的情况有:
- root为p,q中的一个,最近公共祖先为root
- p,q分别在root的左右子树上,最近公共祖先为root
- p和q同时在root的左子树或同时在右子树,则root虽然是公共祖先,但是真正的最近公共祖先在root的下层(确定最近公共祖先需要遍历子树来进行递归求解)
- 递归函数的“灵魂三问”:
①这个函数是干什么的:最终要返回最近公共祖先,递归过程中返回None或p/q/最近公共祖先,用于判断p、q是否在左右子树中
②函数参数是什么:当前所在树的根节点
③得到函数的递归结果,应该干什么:用于判断p、q节点是否在当前结点的左/右子树中
返回结果:
若p、q分别位于左、右子树中,返回root;
左、右子树都没有p、q,返回None;
左、右子树中只有一个含有p/q/p和q,返回那个子树的递归结果(从而保证能返回最近公共祖先)
# Definition for a binary tree node.
# 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':
"""给出根、两个节点,返回最近公共祖先
base case:root就是p、q,直接返回最近公共祖先root
1.(排除base case)root是最近公共祖先的情况,一定是p、q分别位于左、右子树中
2.若左、右子树中都没有p、q,返回None
3.剩下的情况是,左、右子树中的一个含有p/q/p和q,返回那个子树的递归结果(不能返回root,否则不是最近公共祖先)"""
if root is None:
return None
if root == p or root == q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
# 后序遍历位置(从下往上),判断若当前节点左右子树包含p、q,则说明是最近公共祖先
if left and right: # 情况1
return root
if not left and not right: # 情况2
return None
return left if left else right # 情况3