二叉树理论基础
二叉树的分类
满二叉树
如果一棵二叉树只有叶子结点和度为2的结点,并且叶子结点在同一层上,则这棵二叉树为满二叉树
如果满二叉树深度为k,则有个节点
完全二叉树
如果二叉树除了最后一层有缺失外,其它是满的,且最后一层缺失的叶子结点只出现在右侧,则这样的二叉树叫完全二叉树。满二叉树是特殊的完全二叉树。
二叉搜索树
有数值,有序树
- 若某节点的左子树不空,则其左子树上所有节点的值均小于根节点值
- 若某节点的右子树不空,则其右子树上所有结点的值均大于根节点值
- 其左右子树也都是二叉搜索树
平衡二叉搜索树AVL(Adelson-Velsky and Landis)树
它是一棵空树或它的左右两个左子树的高度差绝对值不超过1,并且左右两个子树都是平衡二叉树
二叉树存储方式
链式存储
顺序存储
若父节点的下标为i,则它的左子节点的下标为i*2+1,右子节点的下标为i*2+2
二叉树遍历方式
- 深度优先遍历:先往树的深处走,遇到叶子节点再回退。
实现方式:递归法,利用栈实现迭代遍历(本质上手动实现递归)- 前序遍历:中左右
- 中序遍历:左中右
- 后序遍历:左右中
- 广度优先遍历:一层一层遍历
实现方式:利用队列先进先出实现- 层次遍历
二叉树数据结构定义(链式)
// 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;
}
}
二叉树的遍历方式
二叉树的递归遍历
递归的三要素
- 确定递归函数的参数和返回值
- 确定哪些参数是递归中需要处理的,那么就在递归函数中加上这个参数
- 确定每次递归的返回值是什么,从而确定递归函数的返回类型
- 确定终止条件
- 确定单层递归的逻辑
- 每一层递归要处理的信息
- 重复调用自己实现递归
LeetCode 144 二叉树的前序遍历
思路
前序遍历顺序为==中左右==,考虑递归三要素
- 递归函数的参数和返回值: 全局变量:用于保存递归结果的list 参数:树的根节点root 返回值:空
- 递归函数的结束条件 根节点为空,直接返回
- 单层递归逻辑 把根节点加入list。对左子节点递归,对右子节点递归。
解法
class Solution {
List<Integer> result;
public List<Integer> preorderTraversal(TreeNode root) {
result = new ArrayList<>();
traverse(root);
return result;
}
void traverse(TreeNode root) {
if (root == null) {
return ;
}
result.add(root.val);
traverse(root.left);
traverse(root.right);
}
}
LeetCode 94 二叉树的中序遍历
思路
考虑递归三要素
- 递归函数的参数和返回值 全局变量:保存遍历结果的list 参数:树的根节点root 返回值:空
- 递归函数的结束条件 根节点为空,立即返回
- 单层递归逻辑 对左子节点递归调用。把当前节点加入结果list。对右子节点递归调用。
解法
class Solution {
List<Integer> result;
public List<Integer> inorderTraversal(TreeNode root) {
result = new ArrayList<>();
traverse(root);
return result;
}
void traverse(TreeNode root) {
if (root == null) {
return ;
}
traverse(root.left);
result.add(root.val);
traverse(root.right);
}
}
LeetCode 145 二叉树的后序遍历
思路
后序遍历的顺序是左右中,考虑递归三要素
- 递归函数的参数和返回值 全局变量:保存遍历结果的list 参数:树的根节点root 返回值:空
- 递归函数的结束条件 根节点为空,立即返回
- 单层递归逻辑 对左子节点递归调用,对右子节点递归调用。把当前节点加入结果list
解法
class Solution {
List<Integer> result;
public List<Integer> postorderTraversal(TreeNode root) {
result = new ArrayList<>();
traverse(root);
return result;
}
void traverse(TreeNode root) {
if (root == null) {
return ;
}
traverse(root.left);
traverse(root.right);
result.add(root.val);
}
}
二叉树的迭代遍历
文档讲解:programmercarl.com/二叉树的迭代遍历.ht…
视频讲解: www.bilibili.com/video/BV15f… www.bilibili.com/video/BV1Zf…
二叉树的迭代遍历本质上是用栈去模拟递归遍历。
LeetCode 144 二叉树的前序遍历(迭代法)
思路
前序遍历是中左右,所以右节点先入栈,左节点后入栈
- 把根节点入栈
- 当栈非空,从栈中取出一个元素,把它记录进result。
- 如果其右子节点非空,右子节点入栈
- 如果其左子节点非空,左子节点入栈
解法
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if (root != null) {
stack.push(root);
}
while (!stack.empty()) {
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;
}
}
LeetCode 94 二叉树的中序遍历(迭代法)
思路
中在迭代的过程中,对每个结点其实有两个操作:
- 访问这个节点
- 把节点放入result数组 对于前序遍历,正是因为这两个操作统一,所以比较简便。而中序遍历的顺序是左-中-右,先访问的是顶部节点,先处理的却是左边的节点。所以需要用指针current标识现在访问的节点,栈用来处理节点 处理逻辑是:
- 对每个当前访问的节点,先处理它的左子树,所以下一个访问的节点是它的左子节点
- 对于没有左子节点的,就把当前节点放入数组,再处理它的右子树
算法
- curr置为根节点
- 当curr还有左子节点
- 把curr入栈,把curr指针移到其左子节点。
- 当curr没有左子节点了时,把curr加到数组中。
- 如果curr有右子节点,再把其右子节点置为curr。
- 当curr没有右子节点
- 就弹出栈顶元素作为curr。(但这个curr的左子树都被处理过了,所以我们直接把curr加入数组)
- 把curr加入数组
解法
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode curr = root;
while (curr != null) {
while (curr.left != null) {
stack.push(curr);
curr = curr.left;
}
result.add(curr.val);
while (curr.right == null) {
if (stack.empty()) {
break;
}
curr = stack.pop();
result.add(curr.val);
}
curr = curr.right;
}
return result;
}
}
LeetCode 145 二叉树的后序遍历(迭代法)
思路
后序遍历是左右中,而前序遍历是中左右,反向后是右左中。 所以我们只需要按照中右左的顺序遍历二叉树,把结果反向就得到的后续遍历结果。 因此左子节点先入栈,右子节点后入栈
解法
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if (root != null) {
stack.push(root);
}
while (!stack.empty()) {
TreeNode node = stack.pop();
result.add(node.val);
if (node.left != null) {
stack.push(node.left);
}
if (node.right != null) {
stack.push(node.right);
}
}
return result.reversed();
}
}
二叉树的统一迭代法
即统一处理和访问操作,要处理的和要访问的节点都在栈中,但为了防止重复访问,为访问过的节点推入一个空指针,标识已经访问过但没有处理(访问过指他的左右节点已经入栈)。
于是只有遇到空指针时,才把后面的栈顶节点放入结果数组
二叉树的层序遍历
层序遍历,也就是图论中的广度优先遍历,可以利用先进先出的队列结构实现。 问题:如何在队列里限制对不同层节点的混淆? 在每次对单独一层遍历结束时,队列中有且仅有下一层的全部节点。此时保存队列中元素的个数,访问足够个数的节点后此层遍历结束。
LeetCode 102 二叉树的层序遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty())
int size = queue.size();
List<Integer> nodes = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
nodes.add(node.val);
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
result.add(nodes);
}
return result;
}
}
LeetCode 107 二叉树的层序遍历Ⅱ
偷懒一下,把上面的结果反转就可以了
class Solution {
public List<List<Integer>> levelOrderBottom(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty())
int size = queue.size();
List<Integer> nodes = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
nodes.add(node.val);
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
result.add(nodes);
}
return result.reversed();
}
}
LeetCode 637 二叉树的层平均值
在每层把所有元素加和,单层遍历完就计算平均值加入结果list
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<Double> result = new ArrayList<>();
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty()) {
int size = queue.size();
double sum = 0;
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
sum += node.val;
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
result.add(sum / size);
}
return result;
}
}
LeetCode 199 二叉树的右视图
即返回每层的最后一个节点,只要在遍历每层时把最后一个元素加入结果list
class Solution {
public List<Integer> rightSideView(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<Integer> result = new ArrayList<>();
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (i == size-1) {
result.add(node.val);
}
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
return result;
}
}
LeetCode429:N叉树的层序遍历
只需要把层序遍历中对一个被访问节点子节点的检查拓展到所有子节点。
class Solution {
public List<List<Integer>> levelOrder(Node root) {
Queue<Node> queue = new LinkedList<>();
List<List<Integer>> result = new ArrayList<>();
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> nodes = new ArrayList<>();
for (int i = 0; i < size; i++) {
Node node = queue.poll();
nodes.add(node.val);
for (Node child : node.children) {
queue.offer(child);
}
}
result.add(nodes);
}
return result;
}
}
LeetCode515:在每个树行中找最大值
在遍历每层时记录当前层最大值,遍历完每层把最大值加入结果list
class Solution {
public List<Integer> largestValues(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<Integer> result = new ArrayList<>();
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty()) {
int size = queue.size();
int max = Integer.MIN_VALUE;
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (max < node.val) {
max = node.val;
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(max);
}
return result;
}
}
LeetCode116- 填充每个结点的下一个右侧节点指针
遍历每一层时,把每个节点指向后一个节点即可
class Solution {
public Node connect(Node root) {
Queue<Node> queue = new LinkedList<>();
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty()) {
int size = queue.size();
Node prev = null;
for (int i = 0; i < size; i++) {
Node node = queue.poll();
if (i != 0) {
prev.next = node;
}
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
prev = node;
}
}
return root;
}
}
LeetCode117- 填充每个结点的下一个右侧节点指针Ⅱ
由于上一题是完美二叉树做输入,但算法中并未利用这一性质,故本题也可使用上题答案
LeetCode104:二叉树的最大深度
最大深度即树的层数,层序遍历二叉树记录层数即可
class Solution {
public int maxDepth(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
int result = 0;
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty()) {
result++;
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
return result;
}
}
LeetCode111:二叉树的最小深度
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 也就是当遇到最近叶子节点时,我们就知道了最小深度,此时直接返回所在层的深度即可。
class Solution {
public int minDepth(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
int depth = 0;
if (root != null) {
queue.offer(root);
}
while (!queue.isEmpty()) {
depth++;
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (node.left == null && node.right == null) {
return depth;
}
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
}
return depth;
}
}
今日收获总结
今日学习了四小时,全方面地学习了二叉树的遍历。前后中序遍历主要是理解不同的方法如何实现,层序遍历则是需要灵活运用在遍历过程中层序的特点,记录节点特征信息从而解决问题。