Task03 二叉树
3.1 树与二叉树的基础知识
1. 树简介
- 树的定义:
由n≥0个节点与节点之间的关系组成的有限集合。当n=0时称为空树,当n>0时称为非空树。
树的节点(node):由一个数据元素和若干个指向其子树的树的分支组成。
节点的度:一个节点所含有的子树个数。
根节点(root):有且仅有一个,没有前驱节点的节点。
叶子节点(终端节点leaf node):度为0的节点。
分支节点(非终端节点):度不为0的节点。
子树(subtree): 每个节点包含它所有的后代组成的子树
树的度:树中节点的最大度数。
- 节点间关系:
孩子节点(children):一个节点含有的子树的根节点称为该节点的子节点。
父亲节点(parent):如果一个节点含有子节点,则这个节点称为其子节点的父节点。
兄弟节点(siblings):具有相同父节点的节点互称为兄弟节点。
堂兄弟节点:父节点在同一层的节点互为堂兄弟。
节点的层次:从根节点开始定义,根为第1层,根的子节点为第2层,以此类推。
树的深度(高度):所有节点中最大的层数。
路径(path):树中两个节点之间所经过的节点序列。
路径长度:两个节点之间路径上经过的边数。
节点的祖先:从该节点到根节点所经过的所有节点,被称为该节点的祖先。
节点的子孙:节点的子树中所有节点被称为该节点的子孙。
- 树的分类:
有序树:节点的各个⼦树从左⾄右有序, 不能互换位置。
无序树:节点的各个⼦树可互换位置。
2.二叉树简介
- 二叉树的定义:
树中各个节点的度不大于 2 个的有序树,称为二叉树。通常树中的分支节点被称为 「左子树」 或 「右子树」。二叉树的分支具有左右次序,不能随意互换位置。(可以是空树)
- 特殊的二叉树:
满二叉树:如果所有分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,则称该二叉树为满二叉树。
完全二叉树(Complete Binary Tree):如果叶子节点只能出现在最下面两层,并且最下层的叶子节点都依次排列在该层最左边的位置上,具有这种特点的二叉树称为完全二叉树。
二叉搜索树(Binary Search Tree):也叫做二叉查找树、有序二叉树或者排序二叉树。是指一棵空树或者具有下列性质的二叉树:
平衡二叉搜索树(Balanced Binary Tree):一种结构平衡的二叉搜索树。即叶节点高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉搜索树。平衡二叉树可以在O(logn)内完成插入、查找和删除操作。最早被发明的平衡二叉搜索树为 「AVL 树(Adelson-Velsky and Landis Tree))」。
- 二叉树的存储结构:
顺序存储结构:使用一维数组来存储二叉树中的节点,节点存储位置则采用完全二叉树的节点层次编号,按照层次从上至下,每一层从左至右的顺序依次存放二叉树的数据元素。在进行顺序存储时,如果对应的二叉树节点不存在,则设置为「空节点」。
链式存储结构:每个链节点包含一个用于数据域 val,存储节点信息;还包含两个指针域 left 和 right,分别指向左右两个孩子节点,当左孩子或者右孩子不存在时,相应指针域值为空。
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
3.2二叉树的遍历
二叉树的遍历:指的是从根节点出发,按照某种次序依次访问二叉树中所有节点,使得每个节点被访问一次且仅被访问一次。
1.二叉树的前序遍历:
如果二叉树为空,则返回。
如果二叉树不为空,则:
1.访问根节点。
2.以前序遍历的方式遍历根节点的左子树。
3.以前序遍历的方式遍历根节点的右子树。
前序遍历递归实现:
import typing
from typing import List
from typing import Optional
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
#定义 preorder(root) 表示当前遍历到 root 节点的答案。
def preorder(root):
if not root: #递归终止的条件为碰到空节点
return
res.append(root.val) #先将 root 节点的值加入答案
preorder(root.left) #遍历 root 节点的左子树
preorder(root.right) #遍历 root 节点的右子树即可
res = []
preorder(root)
return res
前序遍历显式栈实现(迭代):
二叉树的前序遍历递归实现的过程,实际上就是调用系统栈的过程。我们也可以使用一个显式栈 stack 来模拟递归的过程。
1.判断二叉树是否为空,为空则直接返回。
2.初始化维护一个栈,将根节点入栈。
3.当栈不为空时:
弹出栈顶元素 node,并访问该元素。
如果 node 的右子树不为空,则将 node 的右子树入栈。
如果 node 的左子树不为空,则将 node 的左子树入栈。
由于栈是“先进后出”的顺序,所以入栈时先将右子树入栈,这样使得前序遍历结果为 “根->左->右”的顺序。
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root: # 二叉树为空直接返回
return []
res = []
stack = [root]
while stack: # 栈不为空
node = stack.pop() # 弹出根节点
res.append(node.val) # 访问根节点
if node.right:
stack.append(node.right) # 右子树入栈
if node.left:
stack.append(node.left) # 左子树入栈
return res
2.二叉树的中序遍历:
如果二叉树为空,则返回。
如果二叉树不为空,则:
1.以中序遍历的方式遍历根节点的左子树。
2.访问根节点。
3.以中序遍历的方式遍历根节点的右子树。
二叉树的中序遍历,一般在二叉搜索树(BST)中最为常用。
中序遍历递归实现:
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
#定义 inorder(root) 表示当前遍历到 root\textit{root}root 节点的答案
def inorder(root):
if not root: #递归终止的条件为碰到空节点
return
inorder(root.left) #遍历 root 节点的左子树
res.append(root.val) #将 root 节点的值加入答案
inorder(root.right) #遍历 root节点的右子树
res = []
inorder(root)
return res
中序遍历显式栈实现:
1.判断二叉树是否为空,为空则直接返回。
2.初始化维护一个空栈。
3.当根节点或者栈不为空时:
如果当前节点不为空,则循环遍历左子树,并不断将当前子树的根节点入栈。
如果当前节点为空,说明当前节点无左子树,则弹出栈顶元素 node,并访问该元素,然后尝试访问该节点的右子树。
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root: # 二叉树为空直接返回
return []
res = []
stack = []
while root or stack: # 根节点或栈不为空
while root:
stack.append(root) # 将当前树的根节点入栈
root = root.left # 找到最左侧节点
node = stack.pop() # 遍历到最左侧,当前节点无左子树时,将最左侧节点弹出
res.append(node.val) # 访问该节点
root = node.right # 尝试访问该节点的右子树
return res
3.二叉树的后序遍历:
如果二叉树为空,则返回。
如果二叉树不为空,则:
1.以后序遍历的方式遍历根节点的左子树。
2.以后序遍历的方式遍历根节点的右子树。
3.访问根节点。
后序遍历递归实现:
class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
#定义 postorder(root) 表示当前遍历到 root 节点的答案。
def postorder(root):
if not root: #递归终止的条件为碰到空节点
return
postorder(root.left) #遍历 root 节点的左子树
postorder(root.right) #遍历 root 节点的右子树
res.append(root.val) #将 root 节点的值加入答案
res = []
postorder(root)
return res
后序遍历显式栈实现:
判断二叉树是否为空,为空则直接返回。
初始化维护一个空栈,使用 prev 保存前一个访问的节点,用于确定当前节点的右子树是否访问完毕。
当根节点或者栈不为空时,从当前节点开始:
如果当前节点有左子树,则不断遍历左子树,并将当前根节点压入栈中。
如果当前节点无左子树,则弹出栈顶元素 node。
如果栈顶元素 node 无右子树(即 not node.right)或者右子树已经访问完毕(即 node.right == prev),则访问该元素,然后记录前一节点,并将当前节点标记为空节点。
如果栈顶元素有右子树,则将栈顶元素重新压入栈中,继续访问栈顶元素的右子树。
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
stack = []
prev = None # 保存前一个访问的节点,用于确定当前节点的右子树是否访问完毕
while root or stack: # 根节点或栈不为空
while root:
stack.append(root) # 将当前树的根节点入栈
root = root.left # 继续访问左子树,找到最左侧节点
node = stack.pop() # 遍历到最左侧,当前节点无左子树时,将最左侧节点弹出
# 如果当前节点无右子树或者右子树访问完毕
if not node.right or node.right == prev:
res.append(node.val)# 访问该节点
prev = node # 记录前一节点
root = None # 将当前根节点标记为空
else:
stack.append(node) # 右子树尚未访问完毕,将当前节点重新压回栈中
root = node.right # 继续访问右子树
return res
4.二叉树的层序遍历:
二叉树的层序遍历是一个**广度优先搜索(BFS)**过程。在遍历的时候是按照第 1 层、第 2 层、…… 最后一层依次遍历的,而同一层节点则是按照从左至右的顺序依次访问的。
二叉树的层序遍历是通过队列来实现的。具体步骤如下:
1.判断二叉树是否为空,为空则直接返回。
2.令根节点入队。
3.当队列不为空时,求出当前队列长度。
4.依次从队列中取出这个元素,并对这个元素依次进行访问。然后将其左右孩子节点入队,然后继续遍历下一层节点。
5.当队列为空时,结束遍历。
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root: #当根节点为空,则返回空列表 []
return []
queue = [root] #根节点的队列
order = [] #结果列表
while queue: #当队列 queue 为空时跳出
level = [] #新建一个临时列表 level ,用于存储当前层打印结果
size = len(queue)
for _ in range(size): #当前层打印循环: 循环次数为当前层节点数(即队列 queue 长度)
curr = queue.pop(0) #队首元素出队
level.append(curr.val) #将节点的值加入答案
if curr.left: #若左子节点不为空,则将左子节点加入队列 queue
queue.append(curr.left)
if curr.right: #若右子节点不为空,则将右子节点加入队列 queue
queue.append(curr.right)
if level: #将当前层结果 level 添加入答案
order.append(level)
return order
3.3二叉树的还原
1.简介
二叉树的还原:指的是通过二叉树的遍历序列,还原出对应的二叉树。
如果已知一棵二叉树的前序序列和中序序列,可以唯一地确定这棵二叉树。
如果已知一棵二叉树的中序序列和后序序列,也可以唯一地确定这棵二叉树。
已知二叉树的「中序遍历序列」和「层序遍历序列」,也可以唯一地确定一棵二叉树。
如果已知二叉树的「前序遍历序列」和「后序遍历序列」,是不能唯一地确定一棵二叉树的。 这是因为没有中序遍历序列无法确定左右部分,也就无法进行子序列的分割。
2.从前序与中序遍历序列构造二叉树
步骤如下:
1.从前序遍历顺序中得到当前根节点的位置在 postorder[0]。
2.通过在中序遍历中查找上一步根节点对应的位置 inorder[k],从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。
3.从上一步得到的左右子树个数将前序遍历结果中的左右子树分开。
4.构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
def createTree(preorder, inorder, n):
if n == 0:
return None
k = 0
while preorder[0] != inorder[k]:
k += 1
node = TreeNode(inorder[k])
node.left = createTree(preorder[1: k + 1], inorder[0: k], k)
node.right = createTree(preorder[k + 1:], inorder[k + 1:], n - k - 1)
return node
return createTree(preorder, inorder, len(inorder))
3. 从中序与后序遍历序列构造二叉树
步骤如下:
1.从后序遍历顺序中当前根节点的位置在 postorder[n-1]。
2.通过在中序遍历中查找上一步根节点对应的位置 inorder[k],从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。
3.从上一步得到的左右子树个数将后序遍历结果中的左右子树分开。
4.构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
def createTree(inorder, postorder, n):
if n == 0:
return None
k = 0
while postorder[n - 1] != inorder[k]:
k += 1
node = TreeNode(inorder[k])
node.right = createTree(inorder[k + 1: n], postorder[k: n - 1], n - k - 1)
node.left = createTree(inorder[0: k], postorder[0: k], k)
return node
return createTree(inorder, postorder, len(postorder))
4. 从前序与后序遍历序列构造二叉树(不要求构造的二叉树是唯一的)
步骤如下:
1.从前序遍历序列中可知当前根节点的位置在 preorder[0]。
2.前序遍历序列的第 2 个值为左子树的根节点,即 preorder[1]。通过在后序遍历中查找上一步根节点对应的位置 postorder[k](该节点右侧为右子树序列),从而将二叉树的左右子树分隔开,并得到左右子树节点的个数。
3.从上一步得到的左右子树个数将后序遍历结果中的左右子树分开。
4.构建当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。
class Solution:
def constructFromPrePost(self, preorder: List[int], postorder: List[int]) -> TreeNode:
def createTree(preorder, postorder, n):
if n == 0:
return None
node = TreeNode(preorder[0])
if n == 1:
return node
k = 0
while postorder[k] != preorder[1]:
k += 1
node.left = createTree(preorder[1: k + 2], postorder[: k + 1], k + 1)
node.right = createTree(preorder[k + 2: ], postorder[k + 1: -1], n - k - 2)
return node
return createTree(preorder, postorder, len(preorder))
二叉树的递归方法使用可以分两类思路:
第一类是直接遍历一遍二叉树,使用前序遍历或后序遍历。
第二类是分解问题,变为通过子问题求解,常常实用后序遍历。
遇到问题是否可以通过遍历一遍二叉树得到答案(前序遍历或后序遍历)?
如果不能的话,是否可以通过分解问题,用子问题(子树)的答案推导出原问题的答案(后序遍历)。
3.5练习(day8)
1. 二叉树的前序遍历
给你二叉树的根节点 root ,返回它节点值的前序遍历。
思路:可使用递归或迭代,详见3.2.1
2. 二叉树的中序遍历
给定一个二叉树的根节点 root ,返回 它的 中序 遍历 。
思路:可使用递归或迭代,详见3.2.2
3. 二叉树的后序遍历
给你一棵二叉树的根节点 root ,返回其节点值的 后序遍历 。
思路:可使用递归或迭代,详见3.2.3
3.6练习(day9)
1. 二叉树的层序遍历
给你二叉树的根节点 root ,返回其节点值的层序遍历 。 (即逐层地,从左到右访问所有节点)。
思路:广度优先搜索(BFS),详见3.2.4
2. 二叉树的最大深度
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
思路:深度优先搜索(DFS),递归遍历二叉树左子树和右子树,取最大深度,加1(根节点)
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root: #递归终止的条件为碰到空节点
return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1 # 递归,左右子树的最大深度
3. 路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。
思路:深度优先搜索,向下寻找叶子节点,当找到叶子节点时sum==0,说明该路径符合要求
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
if not root.left and not root.right:
return targetSum == root.val
return self.hasPathSum(root.left, targetSum - root.val) or self.hasPathSum(root.right, targetSum - root.val)