代码随想录算法训练营第十四天 | 二叉树Part 1: 二叉树理论基础 、二叉树递归遍历、二叉树迭代遍历
今日学习的文章链接和视频链接
参考代码随想录
自己看到题目的第一想法
终于进入二叉树专题,很兴奋也很害怕
自己实现过程中遇到哪些困难
- 二叉树基础知识之前学过,用链表实现二叉树不难,复习了用数组实现二叉树
- 二叉树递归一开始不好理解
今日收获,记录一下自己的学习时长
- 应打卡7月11日,7月17日补打卡,学习时长共6hr
- 复习了二叉树两种遍历深度优先(bfs)和广度优先(dfs)中的bfs的三种方式
- 二叉树bfs递归遍历和迭代遍历都整理出来并理解了,很有成就感
- 二叉树*3,94.二叉树中序遍历、144.二叉树前序遍历、145.二叉树后续遍历
- 继续学习二叉树dfs层序遍历
二叉树 Binary Tree
1. 树基础
树(Tree):由 n > 0 个节点与节点组合组成的有限集合。当 n = 0 时,树被称为空树;当 n > 0 时,树被称为非空树。
1.1 节点分类
- 节点的度:节点所含有子树的个数
- 叶子节点:也叫做 终端节点,度为
0
。 - 分支节点:也叫做 非终端节点,度不为
0
。
1.2 节点间关系
- 子节点:一个节点含有的子树的根节点称为该节点的子节点。
- 父节点:如果一个节点含有子节点,则这个节点称为子节点的父节点。
- 兄弟节点:具有相同父节点的节点互称为兄弟节点。
2. 二叉树基础
二叉树(Binary Tree):树中各个节点度不大于2的有序树称为二叉树。
- 二叉树最多有两个子树,称为「左子树」和「右子树」。
- 二叉树左右子树不可以互换。
2.1 二叉树种类
2.1.1 满二叉树 Full Binary Tree
满二叉树(Full Binary Tree):如果所有分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,则称该二叉树为满二叉树。
- 即满二叉树只有度为0或2的节点,并且度为0的节点在同一层上。
2.1.2 完全二叉树 Complete Binary Tree
完全二叉树(Complete Binary Tree):如果叶子节点只能出现在最下面两层,并且最下层的叶子节点都依次排列在该层最左边的位置上,具有这种特点的二叉树称为完全二叉树。
完全二叉树要满足:
- 叶子节点只能出现在下面两层
- 除了最底层可能没有填满之外,其余每层节点数量达到最大值
- 最下层的叶子节点集中在该层左边
- 若最底层为
h
层(h
从1
开始),则最底层节点个数为1 ~ 2^(h-1)
注意:满二叉树是完全二叉树的一种特殊情况。
2.1.3 二叉搜索树 Binary Search Tree
二叉搜索树是一个有序树,并满足一下性质:
- 对于一个节点来说,如果它的左子树不为空,则左子树上所有节点的值均小于它的根节点的值
- 对于一个节点来说,如果它的右子树不为空,则右子树上所有节点的值均大于它的根节点的值
- 任意节点的左、右子树也是二叉搜索树
2.1.4 平衡二叉搜索树 Balanced Binary Search Tree
平衡二叉搜索树(Balanced Binary Tree):又称为AVL Tree。是一种结构平衡的二叉搜索树。即叶节点高度差的绝对值不超过1,并且左、右两个子树都是一棵平衡二叉搜索树。
AVL树满足以下性质:
- 空二叉树是一个AVL树
- 如果
T
是一个AVL树,它的左、右子树也是AVL树 - AVL树的高度为
O(logn)
2.2 二叉树的存储方式
二叉树的存储结构分为「顺序存储」和「链式存储」。
2.2.1 二叉树顺序存储
- 二叉树顺序存储使用一个一维数组来存储二叉树的节点。
- 节点位置采用完全二叉树的节点层次编号,从上到下,每一层从左到右依次存放二叉树元素。
- 如果对应位置的二叉树元素不存在,则存放「空节点」。
数组和二叉树对应关系:
- 如果某个二叉树节点编号为
i
,且该节点不是叶子节点,其左子节点下标为2 * i + 1
,右子节点下标为2 * i + 2
。 - 如果某个二叉树节点编号为
i
,且该节点是叶子节点,其根节点下标为(i - 1) // 2
顺序存储特点:
- 对于完全二叉树,特别是满二叉树,使用顺序存储能充分利用空间,所以使用顺序存储很合理
- 对于一般二叉树来说,顺序存储要设置很多空节点,会浪费空间
- 二叉树顺序存储时插入、删除操作效率低
2.2.2 二叉树链式存储
- 二叉树链式存储使用链表来存储二叉树的节点
- 每个链节点
node
包含一个数据域val
存储节点信息,包含两个指针域left
和right
,分别指向左子节点和右子节点。
二叉树链式存储的定义:
Python
class TreeNode:
def __init__(self, val, left=None, right=None):
self.val = val
self.left = left
self.right = right
Java
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
// constructor
TreeNode() {}
TreeNode(int val) {this.val = val;}
TreeNode(int val, TreeNode left, TreeNode left) {
this.val = val;
this.left = left;
this.right = right;
}
}
二叉树遍历
二叉树遍历主要有两种方式:「深度优先遍历」和「广度优先遍历」
1 深度优先遍历 Depth-first Search
从二叉树的根节点root
出发,先遍历完根节点的左子树所有节点,然后回到根节点继续遍历根节点的右子树所有节点。整个过程反复进行直到所有节点都被访问完。
深度优先遍历(DFS)可以分为「前序遍历」(Preorder)、「中序遍历」(Inorder)和「后序遍历」(Postorder)。
- 前序遍历:「中」 -> 左 -> 右
- 中序遍历:左 -> 「中」 -> 右
- 后序遍历:左 -> 右 -> 「中」
例如:
前序遍历:5 4 1 2 6 7 8
中序遍历:1 4 2 5 7 6 8
后序遍历:1 2 4 7 8 6 5
1.1 前序遍历 Preorder Traversal
前续遍历规则:
- 如果树为空,则返回
- 如果树不为空,则:
- 访问根节点
- 按照前续遍历方式遍历根节点的左子树
- 按照前续遍历方式遍历根节点的右子树
所以前序遍历顺序为:中 -> 左 -> 右
1.1.1 前序遍历的递归实现
递归写法的三要素:
- 确定递归的 参数和返回值
- 确定递归的 终止条件
- 确定 单层递归的逻辑
前序遍历的递归实现:
- 确定递归的 参数和返回值: 需要传入的参数为当前节点,因为要打印出前序遍历节点的值,所以参数需要一个数组来存放前序遍历节点的值,不需要返回值
# LC144-二叉树的前序遍历
def preorder(cur: Optional[TreeNode], res: List[int]) -> None:
- 确定递归的 终止条件:遍历过程中,如果当前节点为空,说明本层遍历结束,所以终止条件为当前节点为空就返回
if cur == None: return
- 确定 单层递归的逻辑:前序遍历顺序为中左右,所以单层递归时,要先取中节点的值,然后左,然后右
res.append(cur.val) # 中
preorderTraversal(cur.left, res) # 左
preorderTraversal(cur.right, res) # 右
完整代码:
1.1.1.1 Python - 前序遍历的递归实现
# 前序遍历-递归-LC144_二叉树的前序遍历
# 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 preorderTraversal(self, root: TreeNode) -> List[int]:
# 前序遍历的递归实现
res = list()
def preorder(cur: TreeNode, vec: List[int]) -> None:
# 终止条件
if not cur: return
vec.append(cur.val) # 中
preorder(cur.left, vec) # 左
preorder(cur.right, vec) # 右
preorder(root, res)
return res
1.1.1.2 Java - 前序遍历的递归实现
// 前序遍历-递归-LC144_二叉树的前序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
// 前序遍历的递归实现
List<Integer> result = new LinkedList<>();
preorder(root, result);
return result;
}
public void preorder(TreeNode cur, List<Integer> temp) {
// 终止条件
if (cur == null) {
return;
}
temp.add(cur.val); // 中
preorder(cur.left, temp); // 左
preorder(cur.right, temp); // 右
}
}
1.1.2 前序遍历的迭代实现
迭代实现的基本思路:
- 使用递归就是每一次递归调用都把函数的局部变量、参数、返回值压入栈中,然后等递归返回的时候,从栈顶弹出上一层的各项参数,返回上一层。
- 所以迭代实现递归算法时需要用栈。
前序遍历的迭代实现:
每次遍历时,处理完中间节点后,先把 右节点 压入栈中,然后再把 左节点 压入栈中,这样出栈的时候是 中左右 的顺序。
注意:中节点不入栈
具体算法:
- 判断二叉树是否为空,如果为空返回
- 初始化返回数组
res
- 初始化空栈
stack
,把根节点root
压入栈中 - 当栈
stack
不为空时:- 栈弹出当前栈顶元素
node
,并处理该元素,即res
记录该节点的值 - 如果
node
右节点不为空,访问右节点并入栈 - 然后(注意顺序),如果
node
左节点不为空,访问左节点并入栈
- 栈弹出当前栈顶元素
1.1.2.1 Python - 前序遍历的迭代实现
# 前序遍历-递归-LC144_二叉树的前序遍历
# 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 preorderTraversal(self, root: TreeNode) -> List[int]:
# 前序遍历的迭代实现
# 返回数组
res = []
# 空树
if not root:
return res
# list as stack
# root只在第一次入栈,之后迭代过程中,root不入栈
stack = [root]
# 终止条件为stack为空,说明遍历完树
while stack: # 相当于 len(stack) != 0
node = stack.pop() # 弹出根节点
res.append(node.val) # 访问根节点
if node.right:
stack.append(node.right) # 右子树先入栈
if node.left:
stack.append(node.left) # 左子树后入栈
return res
1.1.2.2 Java - 前序遍历的迭代实现
// 前序遍历-递归-LC144_二叉树的前序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
// 前序遍历的迭代实现
List<Integer> result = new LinkedList<>();
// 空树
if (root == null) {
return result;
}
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
result.add(node.val);
// 先右入栈
if (node.right != null) {
stack.push(node.right);
}
// 后左入栈
if (node.left != null) {
stack.push(node.left);
}
}
return result;
}
}
1.2 中序遍历 Inorder Traversal
中续遍历规则:
- 如果树为空,则返回
- 如果树不为空,则:
- 按照中续遍历方式遍历根节点的左子树
- 访问根节点
- 按照中续遍历方式遍历根节点的右子树
所以中序遍历顺序为:左 -> 中 -> 右
1.2.1 中序遍历的递归实现
中序遍历递归的基本思路与前序遍历递归一致。
中序遍历的递归实现:
- 确定递归的 参数和返回值: 需要传入的参数为当前节点,因为要打印出中序遍历节点的值,所以参数需要一个数组来存放前序遍历节点的值,不需要返回值
# LC094-二叉树的中序遍历
def inorder(cur: Optional[TreeNode], res: List[int]) -> None:
- 确定递归的 终止条件:遍历过程中,如果当前节点为空,说明本层遍历结束,所以终止条件为当前节点为空就返回
if cur == None: return
- 确定 单层递归的逻辑:中序遍历顺序为左中右,所以单层递归时,要先取根节点的左子树的值,然后根节点,最后右子树
inorderTraversal(cur.left, res) # 左
res.append(cur.val) # 中
inorderTraversal(cur.right, res) # 右
完整代码:
1.2.1.1 Python - 中序遍历的递归实现
# 中序遍历-递归-LC094_二叉树的中序遍历
# 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 inorderTraversal(self, root: TreeNode) -> List[int]:
# 前序遍历的递归实现
res = list()
def inorder(cur: TreeNode, vec: List[int]) -> None:
# 终止条件
if not cur: return
inorder(cur.left, vec) # 左
vec.append(cur.val) # 中
inorder(cur.right, vec) # 右
inorder(root, res)
return res
1.2.1.2 Java - 中序遍历的递归实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(root, res);
return res;
}
public void inorder(TreeNode cur, List<Integer> temp) {
// 终止条件
if (cur == null) {
return;
}
inorder(cur.left, temp); // 左
temp.add(cur.val); // 中
inorder(cur.right, temp); // 右
}
}
1.2.2 中序遍历的迭代实现
- 中序遍历的迭代实现思路与前序遍历不同
- 前序遍历迭代中一共有两个步骤:
- 访问:从根节点
root
开始,不断向下遍历,将遍历到的元素放入栈stack
中 - 处理:栈
stack
中弹出栈顶节点,记录栈顶节点的值
- 访问:从根节点
- 前序遍历的顺序是中左右,访问的节点和处理的节点是同一个节点
- 中序遍历的顺序是左中右,从根节点
root
开始,一层层往下遍历直到树的最底部,然后才开始处理节点;所以在中序遍历中,需要保证在左子树访问完之前,当前的元素不能出栈
具体算法:
- 判断二叉树是否为空,如果为空返回
- 初始化空栈
stack
,初始化返回数组res
- 当前节点或栈不为空时:
- 如果当前节点不为空,循环遍历当前节点的左子树,并将当前子树的根节点加入栈中
- 如果当前节点为空,说明已经遍历到子树的左边最底部,栈
stack
弹出栈顶元素node
,并记录该元素的值;然后将访问当前栈顶元素的右子树
- 注意循环条件为当前节点
cur
不为空或者栈stack
不为空,即while cur or stack
- 如果当前节点
cur
不为空,但是栈stack
为空,说明当前节点cur
是一个当前子树的根节点,还需要继续向下访问(继续向下遍历) - 如果当前节点
cur
为空,但是栈stack
不为空,说明已经遍历到当前子树的最底层,需要开始处理节点(即栈弹出栈顶元素并记录) - 只有当前节点
cur
为空和栈stack
也为空时,说明整棵树都遍历到,遍历结束
- 如果当前节点
1.2.2.1 Python - 中序遍历的迭代实现
# 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 inorderTraversal(self, root: TreeNode) -> List[int]:
res = list()
# 空树
if not root:
return res
# list as stack
stack = list()
cur = root
while cur or stack:
if cur:
# 从根节点依次向下访问
stack.append(cur)
cur = cur.left
else:
# not cur: 说明遍历到子树的最左节点
# 处理当前节点并访问当前节点的右子树
cur = stack.pop()
res.append(cur.val)
cur = cur.right
return res
1.2.2.2 Java - 中序遍历的迭代实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
// 空树
if (root == null) {
return res;
}
// deque as stack
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
// 当前节点不为空或栈不为空
while (cur != null || !stack.isEmpty()) {
if (cur != null) {
// 继续向下访问
stack.push(cur);
cur = cur.left;
} else {
// 处理栈顶节点,并访问栈顶节点右子树
cur = stack.pop();
res.add(cur.val);
cur = cur.right;
}
}
return res;
}
}
1.3 后序遍历 Postorder Traversal
后序遍历规则:
- 如果树为空,则返回
- 如果树不为空,则:
- 按照后序遍历方式遍历根节点的左子树
- 按照后序遍历方式遍历根节点的右子树
- 访问根节点
所以后序遍历顺序为:左 -> 右 -> 中
1.3.1 后序遍历的递归实现
后序遍历递归的基本思路与前序遍历递归、中序遍历递归一致。
后序遍历的递归实现:
- 确定递归的 参数和返回值: 需要传入的参数为当前节点,因为要打印出后序遍历节点的值,所以参数需要一个数组来存放前序遍历节点的值,不需要返回值
# LC145-二叉树的后序遍历
def postorder(cur: Optional[TreeNode], res: List[int]) -> None:
- 确定递归的 终止条件:遍历过程中,如果当前节点为空,说明本层遍历结束,所以终止条件为当前节点为空就返回
if cur == None: return
- 确定 单层递归的逻辑:后序遍历顺序为左右中,所以单层递归时,要先取根节点的左子树的值,然后右子树的值,最后根节点
postorderTraversal(cur.left, res) # 左
postorderTraversal(cur.right, res) # 右
res.append(cur.val) # 中
完整代码:
1.3.1.1 Python - 后序遍历的递归实现
# 后序遍历-递归-LC145_二叉树的后序遍历
# 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 postorderTraversal(self, root: TreeNode) -> List[int]:
# 后序遍历的递归实现
res = list()
def postorder(cur: TreeNode, vec: List[int]) -> None:
# 终止条件
if not cur: return
postorder(cur.left, vec) # 左
postorder(cur.right, vec) # 右
vec.append(cur.val) # 中
postorder(root, res)
return res
1.3.1.2 Java - 后序遍历的递归实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
postorder(root, res);
return res;
}
public void postorder(TreeNode cur, List<Integer> temp) {
// 终止条件
if (cur == null) {
return;
}
postorder(cur.left, temp); // 左
postorder(cur.right, temp); // 右
temp.add(cur.val); // 中
}
}
1.3.2 后序遍历的迭代实现
- 后续遍历的迭代实现基本思路与前序遍历一致
- 后续遍历的顺序是左右中,前序遍历的顺序是中左右,具体更改如下:
- 在原本前序遍历的基础上,先修改访问和处理节点的顺序,更改为左子树入栈,再右子树入栈,此时访问的顺序(即出栈的顺序)变为 中右左
- 最后返回数组
res
时,反转数组,顺序变为 左右中
具体算法:
- 判断二叉树是否为空,如果为空返回
- 初始化返回数组
res
- 初始化空栈
stack
,把根节点root
压入栈中 - 当栈
stack
不为空时:- 处理栈顶元素,栈弹出当前栈顶元素
node
,res
记录该节点的值 - 如果
node
左节点不为空,访问左节点并入栈 - 然后(注意顺序),如果
node
右节点不为空,访问右节点并入栈
- 处理栈顶元素,栈弹出当前栈顶元素
- 此时返回数组
res
的顺序为中右左,最后反转返回数组res
,顺序为左右中
1.3.2.1 Python - 后序遍历的迭代实现
# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = list()
# 空树
if not root:
return res
# list as stack
stack = [root]
while stack: # 出栈顺序:中右左
# 中
node = stack.pop()
res.append(node.val)
# 左
if node.left:
stack.append(node.left)
# 右
if node.right:
stack.append(node.right)
# 反转res:左右中
return res[::-1]
1.3.2.2 Java - 后序遍历的迭代实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
// 空树
if (root == null) {
return res;
}
// deque as stack
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
// 出栈顺序:中右左
while (!stack.isEmpty()) {
// 中
TreeNode node = stack.pop();
res.add(node.val);
// 左
if (node.left != null) {
stack.push(node.left);
}
// 右
if (node.right != null) {
stack.push(node.right);
}
}
// 反转res:左右中
Collections.reverse(res);
return res;
}
}