一、理论基础
1、二叉树的种类
(1)满二叉树
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。
(2)完全二叉树
完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。,若最底层为第h层,则该层包含1~2^h-1个节点。
(3)二叉搜索树
前面的树都没有数值的,而二叉搜索树是有数值的,二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树。
(4)平衡二叉搜索树
平衡二叉搜索树:又被称为AVL树,且具有性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
2、二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。那么链式存储⽅式就⽤指针, 顺序存储的⽅式就是⽤数组。
链式存储:
顺序存储:
3、二叉树的遍历方式
有两种:
a.深度优先遍历:先往深走,遇到叶子节点再往回走。
- 前序遍历:中左右
- 中序遍历:左中右
- 后序遍历:左右中
这里的前中后,其实指的是中间结点的遍历顺序。
b:广度优先遍历:层次遍历
二、二叉树的遍历
1、二叉树的递归遍历
递归算法要确定的三要素:
- 确定递归函数的参数和返回值:确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数,并且还要明确每次递归的返回值是什么,进而确定递归函数的返回类型。
- 确定终止条件:写完递归算法,运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然会溢出。
- 确定单层递归的逻辑。
前中后序遍历的递归法python代码:
# 前序遍历-递归-LC144_二叉树的前序遍历
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
# 保存结果
result = []
def traversal(root: TreeNode):
if root == None:
return
result.append(root.val) # 前序
traversal(root.left) # 左
traversal(root.right) # 右
traversal(root)
return result
# 中序遍历-递归-LC94_二叉树的中序遍历
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
result = []
def traversal(root: TreeNode):
if root == None:
return
traversal(root.left) # 左
result.append(root.val) # 中序
traversal(root.right) # 右
traversal(root)
return result
# 后序遍历-递归-LC145_二叉树的后序遍历
class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
result = []
def traversal(root: TreeNode):
if root == None:
return
traversal(root.left) # 左
traversal(root.right) # 右
result.append(root.val) # 后序
traversal(root)
return result
力扣上题目是关于前中后序的:
- 144.二叉树的前序遍历
- 145.二叉树的后序遍历
- 94.二叉树的中序遍历
- 589.N叉树的前序遍历
- 590.N叉树的后序遍历
2、二叉树的迭代遍历
也可以用迭代法来解决上面三道题目:因为递归的实现就是每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
(1)前序遍历(迭代法)
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。这样出栈的时候才是中左右的顺序。
迭代的过程中,有两个操作:
- 处理:将元素放进result数组中
- 访问:遍历节点
(2)中序遍历
中序遍历和刚才的前序遍历不能通用的原因在于:因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点。要访问的元素和要处理的元素顺序是一致的,都是中间节点。那么中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层往下访问,直到到达左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
(3)后序遍历
再来看后序遍历,先序遍历是中左右,后序遍历是左右中,那么只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后再反转result数组,输出的结果顺序就是左右中了。
前中后序遍历的迭代法python代码:
# 前序遍历-迭代-LC144_二叉树的前序遍历
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
# 根结点为空则返回空列表
if not root:
return []
stack = [root]
result = []
while stack:
node = stack.pop()
# 中结点先处理
result.append(node.val)
# 右孩子先入栈
if node.right:
stack.append(node.right)
# 左孩子后入栈
if node.left:
stack.append(node.left)
return result
# 中序遍历-迭代-LC94_二叉树的中序遍历
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
if not root:
return []
stack = [] # 不能提前将root结点加入stack中
result = []
cur = root
while cur or stack:
# 先迭代访问最底层的左子树结点
if cur:
stack.append(cur)
cur = cur.left
# 到达最左结点后处理栈顶结点
else:
cur = stack.pop()
result.append(cur.val)
# 取栈顶元素右结点
cur = cur.right
return result
# 后序遍历-迭代-LC145_二叉树的后序遍历
class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
if not root:
return []
stack = [root]
result = []
while stack:
node = stack.pop()
# 中结点先处理
result.append(node.val)
# 左孩子先入栈
if node.left:
stack.append(node.left)
# 右孩子后入栈
if node.right:
stack.append(node.right)
# 将最终的数组翻转
return result[::-1]
3、二叉树的层序遍历
层序遍历一个二叉树,就是从左到右一层一层的去遍历二叉树。需要借助一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。而这种层序遍历方式就是图论中的广度优先遍历,只不过应用在二叉树上。
使用队列实现二叉树广度优先遍历:
层序遍历python代码:
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root:
return []
quene = [root]
out_list = []
while quene:
length = len(queue)
in_list = []
for _ in range(length):
curnode = queue.pop(0) # (默认移除列表最后一个元素)这里需要移除队列最头上的那个
in_list.append(curnode.val)
if curnode.left: queue.append(curnode.left)
if curnode.right: queue.append(curnode.right)
out_list.append(in_list)
return out_list
力扣上对应题目:
- 102.二叉树的层序遍历
- 107.二叉树的层序遍历II
- 199.二叉树的右视图
- 637.二叉树的层平均值
- 429.N叉树的前序遍历
- 515.在每个树行中找最大值
- 116.填充每个节点的下一个右侧节点指针
- 117.填充每个节点的下一个右侧节点指针II
还有一道非常经典的题目:226.翻转二叉树
4.二叉树的深度
(1)最大深度:
力扣上题目:
- 104.二叉树的最大深度
- 559.N叉树的最大深度
(2)最小深度
力扣上题目:
- 二叉树的最小深度
二叉树总结的思维导图:
三、二叉树所要掌握的技能
1、二叉树的理论基础
二叉树的种类、存储方式、遍历方式、定义方式
2、二叉树的遍历方式
- 深度优先遍历
二叉树:前中后序递归法(递归三要素)
二叉树:前中后序迭代法(通过栈模拟递归)
二叉树:前中后序迭代法(统一风格)
- 广度优先遍历
二叉树的层序遍历:通过队列模拟
3、求二叉树的属性
(1)是否对称
- 递归:后序,比较的是根节点的左子树与右子树是不是相互翻转
- 迭代:使用队列/栈将两个节点顺序放入容器中进行比较
(2)求最大深度
- 递归:后序,求根节点最大高度就是最大深度,通过递归函数的返回值做计算树的高度
- 迭代:层序遍历
(3)求最小深度
- 递归:后序,求根节点最小高度就是最小深度,注意最小深度的定义
- 迭代:层序遍历
(4)求有多少个节点
- 递归:后序,通过递归函数的返回值计算节点数量
- 迭代:层序遍历
(5)是否平衡
- 递归:后序,注意后序求高度和前序求深度,递归过程判断高度差
- 迭代:效率很低,不推荐
(6)找所有路径
- 递归:前序,方便让父节点指向子节点,涉及回溯处理根节点到叶子的所有路径
- 迭代:一个栈模拟递归,一个栈来存放对应的遍历路径
(7)递归中如何隐藏着回溯
(8)求左叶子之和
- 递归:后序,必须三层约束条件,才能判断是否是左叶子
- 迭代:直接模拟后序遍历
(9)求左下角的值
- 递归:顺序无所谓,优先左孩子搜索,同时找深度最大的叶子节点
- 迭代:层序遍历找最后一行最左边
(10)求路径总和
- 递归:顺序无所谓,递归函数返回值为bool类型是为了搜索一条边,没有返回值是搜索整棵树
- 迭代:栈里元素不仅要记录节点指针,还要记录从头节点到该节点的路径数值总和
4、二叉树的修改与构造
(1)翻转二叉树
- 递归:前序,交换左右孩子
- 迭代:直接模拟前序遍历
(2)构造二叉树
- 递归:前序,重点在于找分割点,分左右区间构造
- 迭代:比较复杂,意义不大
(3)构造最大的二叉树
- 递归:前序,分割点为数组最大值,分左右区间构造
- 迭代:比较复杂,意义不大
(4)合并两个二叉树
- 递归:前序,同时操作两个树的节点,注意合并的规则
- 迭代:使用队列,类似层序遍历
5、求二叉搜索树的属性
(1)二叉搜索树中的搜索
- 递归:二叉搜索树的递归是有方向的
- 迭代:因为有方向,所以迭代法很简单
(2)是不是二叉搜索树
- 递归:中序,相当于变成了判断一个序列是不是递增的
- 迭代:模拟中序,逻辑相同
(3)求二叉搜索树的最小绝对差
- 递归:中序,双指针操作
- 迭代:模拟中序,逻辑相同
(4)求二叉搜索树的众数
- 递归:中序,清空结果集的技巧,遍历一遍便可求众数集合
- 迭代:模拟中序,逻辑相同
(5)二叉搜索树转成累加树
- 递归:中序,双指针操作累加
- 迭代:模拟中序,逻辑相同
6、二叉树公共祖先问题
(1)二叉树的公共祖先问题
- 递归:后序、回溯,找到左子树出现目标值,右子树节点目标值的节点
- 迭代:不适合模拟回溯
(2)二叉搜索树的公共祖先问题
- 递归:顺序无所谓,如果节点的数值在目标区间就是最近公共祖先
- 迭代:按序遍历
7、二叉搜索树的修改与构造
(1)二叉搜索树中的插入操作
- 递归:顺序无所谓,通过递归函数返回值添加节点
- 迭代:按序遍历,需要记录插入父节点,这样才能做插入操作
(2)二叉搜索树中的删除操作
- 递归:前序,想清楚删除非叶子节点的情况
- 迭代:有序遍历,较复杂
(3)修剪二叉搜素树
- 递归:前序,通过递归函数返回值删除节点
- 迭代:有序遍历,较复杂
(4)构造二叉搜索树
- 递归:前序,数组中间节点分割
- 迭代:较复杂,通过三个队列来模拟
四、总结
- 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点
- 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算
- 求二叉搜索树的属性,一定是中序,要利用有序性
注意在普通二叉树的属性中,一般用后序,例如单纯求深度就用前序,二叉树:找所有路径也用前序,旨在让父节点指向子节点。