01.二叉树是否对称、相同
对称和相同的处理框架相同,迭代法使用辅助队列,递归法,对应递归的孩子节点;
练习题:
对称是关于根节点的轴对称,辅助队列,成对入队进行比较,辅助队列可以存取空节点;
迭代法,辅助队列,成对入队,成对比较,可以存入空值,增加对应的处理即可
// 辅助队列,成对入队,成对比较,可以存入空值,增加对应的处理即可
public boolean isSymmetric(TreeNode root) {
// 如果根节点为空,默认对称
if (root == null) {
return true;
} // 否则树非空,左右子树成对处理
Queue<TreeNode> queue = new LinkedList<>();
// 初始化辅助队列,此时可能存入空节点,所以迭代时,要增加判断,再取值
queue.offer(root.left);
queue.offer(root.right);
// 循环条件是队列非空
while (!queue.isEmpty()) {
// 获取当前节点
TreeNode currLeft = queue.poll();
TreeNode currRight = queue.poll();
// 罗列当前节点的所有情况:
// 01.两个节点都为空,无需处理
// 02.一个节点为空,一个节点非空,返回 false
// 03.两个节点都非空,且节点值不同,返回 false
// 04.两个节点都非空,且节点值相同,继续遍历其孩子节点,成对入队
if ((currLeft == null) && (currRight == null)) {
// 01.两个节点都为空,无需处理
continue; // 无孩子节点存入,继续遍历其他节点
} else if ((currLeft != null) && (currRight == null)) {
// 02.一个节点为空,一个节点非空,返回 false
return false;
} else if ((currLeft == null) && (currRight != null)) {
// 02.一个节点为空,一个节点非空,返回 false
return false;
} else if (currLeft.val != currRight.val) { // 此处节点都存在,可以取值
// 03.两个节点都非空,且节点值不同,返回 false
return false;
} else {
// 04.两个节点都非空,且节点值相同,继续遍历其孩子节点,成对入队
queue.offer(currLeft.right); // 内侧
queue.offer(currRight.left);
queue.offer(currLeft.left); // 外侧
queue.offer(currRight.right);
}
}
// 遍历结束,全部符合
return true;
}历结束,符合条件
return true;
}
}
递归法,递归孩子节点后,注意对称是所有节点都对称,即 &&
递归时,可以利用 && 的短路性质进行提速,即将方式01修改为方式02;
// 方式01
// 成对递归其孩子节点
boolean inner = assist(currLeft.right, currRight.left);
boolean outer = assist(currLeft.left, currRight.right);
return inner && outer; // 都对称才对称
// 方式02,会因为短路,不去计算后边的表达式
return assist(currLeft.right, currRight.left)&& assist(currLeft.left, currRight.right); // 都对称才对称
// 递归法,三要素
public boolean isSymmetric(TreeNode root) {
// 处理空树
if (root == null) {
return true;
} else {
return assist(root.left, root.right);
}
}
// 01.确定输入参数:待比较的当前节点 和 返回值:是否对称的累积结果
// 02.终止条件:当前节点为空,此路径遍历结束,对称,返回 true
// 03.单层操作:比较当前节点数值,对应递归孩子节点
public boolean assist(TreeNode currLeft, TreeNode currRight) {
// 终止条件:当前节点为空,此路径遍历结束,对称
if ((currLeft == null) && (currRight == null)) {
return true;
} else if ((currLeft != null) && (currRight == null)) { // 单层操作
return false;
} else if ((currLeft == null) && (currRight != null)) {
return false;
} else if (currLeft.val != currRight.val) { // 此处当前节点都存在
return false;
} else { // 此处当前节点都存在,且数值相等
// 成对递归其孩子节点
boolean inner = assist(currLeft.right, currRight.left);
boolean outer = assist(currLeft.left, currRight.right);
return inner && outer; // 都对称才对称
}
}
如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的,框架同上,对应的节点不同而已;
02.平衡二叉树
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1;
二叉树节点的深度:指从根节点到该节点的最长简单路径边的节点个数;
二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的节点个数,即当前节点的最大深度;
练习题:
前序遍历 + 层序遍历的应用;前序遍历,判断每一个节点左右子树的高度差;层序遍历计算高度差;
注意后序遍历时间会大于前序遍历,推荐使用前序遍历;
03.完全二叉树节点的个数
已知满二叉树的层数为 n,则满二叉树节点的个数等于:
2 n − 1 2^n - 1 2n−1
获取完全二叉树的高度,可以直接左孩子一条路走到头,即:currNode = currNode.left 进行更新;原因:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置;
利用完全二叉树的性质,如果当前节点的左子树高度和右子树相同,则左子树一定为满二叉树;
练习题:
// 递归法:
// 01.确定输入参数:当前节点 和 返回值:左子树满二叉树 + 根节点的数量
// 02.终止条件是当前节点为空
// 03.单层操作,如果左右子树的深度相同,则左子树一定是满二叉树,否则左右子树都不是满二叉树,同时递归
public int countNodes(TreeNode root) {
if (root == null) {
return 0;
}
// 单层操作,计算左右子树的高度
int leftHeight = getHeight(root.left);
int rightHeight = getHeight(root.right);
if (leftHeight == rightHeight) { // 如果左子树是满二叉树
// 计算左子树的节点个数 + 当前节点 = Math.pow(2, leftHeight) - 1 + 1
int nodesNum = (int) Math.pow(2, leftHeight);
// 还要继续递归右子树
return nodesNum + countNodes(root.right);
} else { // 左子树不满二叉树,左右孩子节点都需要递归
// 注意加上当前节点本身的数量
return 1 + countNodes(root.left) + countNodes(root.right);
}
}
public int getHeight(TreeNode root) {
int depth = 0;
// 循环条件是当前节点非空
while (root != null) {
depth++; // 更新深度
root = root.left; // 更新当前节点
}
return depth;
}
04.二叉树的路径
前序遍历方便路径的生成;
练习题:
迭代法,记录路径时,记录的是当前节点对应路径的字符串,这样就避免访问顺序和每次叶子节点都需要重新计算路径;
// 迭代法,前、中、后序遍历都可,使用辅助栈,成对存取当前节点及其对应的路径
// 注意:现存后取 和 强制转换
public List<String> binaryTreePaths(TreeNode root) {
// 创建存储结果的列表
List<String> results = new LinkedList<>();
// 如果根节点非空,则需要遍历路径
if (root != null) {
// 创建辅助栈
Deque<Object> stack = new LinkedList<>();
// 将根节点及其路径入栈
stack.push(root);
stack.push(String.valueOf(root.val));
// 逐个遍历每个节点,循环条件是栈非空
while (!stack.isEmpty()) {
// 获取当前节点 及其 对应的路径,先存后取
String currPath = (String) stack.pop();
TreeNode currNode = (TreeNode) stack.pop();
// 如果当前节点非空,则按顺序入栈,以前序为例,中左右出栈,则右左中入栈
if (currNode != null) {
// 将非空节点入栈
if (currNode.right != null) {
stack.push(currNode.right);
stack.push(currPath + "->" + currNode.right.val);
}
if (currNode.left != null) {
stack.push(currNode.left);
stack.push(currPath + "->" + currNode.left.val);
}
stack.push(currNode);
stack.push(currPath);
stack.push(null);
stack.push(null);
} else { // 如果当前节点为空,则处理
currPath = (String) stack.pop();
currNode = (TreeNode) stack.pop();
// 如果是叶子节点,则记录完整路径
if ((currNode.left == null) && (currNode.right == null)) {
results.add(currPath);
}
}
}
}
return results;
}
递归法,使用参数传递当前节点对应的路径字符串;
// 递归法,三要素
public List<String> binaryTreePaths(TreeNode root) {
// 创建记录结果的列表
List<String> paths = new LinkedList<>();
// 处理非空树
if (root != null) {
String path = String.valueOf(root.val);
assist(root, path, paths);
}
return paths;
}
// 01.确定输入参数:当前节点、当前节点对应的路径字符串、记录所有路径的列表 和 返回值:无
// 02.终止条件:当前节点为叶子节点,记录此条路径
// 03.单层操作:递归时,需要判断节点非空
public void assist(TreeNode currNode, String currPath, List<String> paths) {
// 终止条件:当前节点为叶子节点,记录此条路径
if ((currNode.left == null) && (currNode.right == null)) {
paths.add(currPath);
return; // 结束方法
}
// 单层操作
if (currNode.left != null) {
assist(currNode.left, currPath + "->" + currNode.left.val, paths);
}
if (currNode.right != null) {
assist(currNode.right, currPath + "->" + currNode.right.val, paths);
}
}
05.二叉树的左叶子之和
左叶子之和是指二叉树中所有是左孩子的叶子节点的数值之和,即左叶子节点的判断条件是:01.当前节点的左孩子节点存在,02.该左孩子节点是否为叶子节点;
练习题:
递归法,如果当前节点的左孩子是叶子节点就累积,否则当前值用0累积;
// 递归法,所有的遍历都可,即在每个节点处理,判断当前节点的左孩子节点是否存在,且为左叶子节点
// 以前序遍历为例,中左右
// 01.确定输入参数:当前节点 和 返回值:所有左叶子节点的累积和
// 02.终止条件是当前节点为空
// 03.单层操作:判断当前节点的左孩子节点是否存在,是否为叶子节点,累积,递归其孩子节点
public int sumOfLeftLeaves(TreeNode root) {
// 终止条件
if (root == null) {
return 0;
}
// 单层操作
int currLeftLeaf = 0;
if ((root.left != null) && (root.left.left == null) && (root.left.right == null)) {
currLeftLeaf += root.left.val;
}
// 递归孩子节点
return currLeftLeaf + sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right);
}
迭代法,DFS的统一框架,处理位置,增加判断,如果当前节点的左孩子是叶子节点,则累积此左孩子的数值;
// 迭代法,所有遍历均可,以中序遍历为例,辅助栈,模拟递归,左中右出栈,右中左入栈
public int sumOfLeftLeaves(TreeNode root) {
// 初始化所有左叶子之和
int sum = 0;
// 创建辅助栈
Deque<TreeNode> stack = new LinkedList<>();
// 如果根节点非空,将其入栈
if (root != null) {
stack.push(root);
}
// 逐个遍历节点,循环条件是栈非空
while (!stack.isEmpty()) {
// 获取当前节点
TreeNode currNode = stack.pop();
// 如果当前节点非空,则模拟递归,按“右中左”顺序入栈
if (currNode != null) {
// 将非空的孩子节点入栈
if (currNode.right != null) {
stack.push(currNode.right);
}
stack.push(currNode);
stack.push(null);
if (currNode.left != null) {
stack.push(currNode.left);
}
} else { // 如果当前节点为空,则处理下一节点
currNode = stack.pop();
// 判断当前节点的左孩子节点是否为左叶子节点
if ((currNode.left != null) && (currNode.left.left == null) && (currNode.left.right == null)) {
sum += currNode.left.val;
}
}
}
return sum;
}
06.路径总和为目标值
左叶子之和是指二叉树中所有是左孩子的叶子节点的数值之和,即左叶子节点的判断条件是:01.当前节点的左孩子节点存在,02.该左孩子节点是否为叶子节点;
练习题:
存在即找到一条路径所有节点数据之和为目标值即可;
找到所有的路径节点数据之和为目标值的所有路径,即需要遍历每个节点,记录结果时,注意深度拷贝,避免结果被其他过程修改;
递归法,终止条件是当前节点为叶子节点,需要特殊处理空树,并判断递归的孩子节点是否为空;
// 创建记录符合条件路径的列表
List<List<Integer>> paths = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
// 处理非空树
if (root != null) {
List<Integer> path = new LinkedList<>();
path.add(root.val);
assist(root, targetSum, path);
}
return paths;
}
// 递归法:
// 01.确定输入参数:当前节点、当前目标、当前节点对应的路径 和 返回值:无
// 02.当前节点为叶子节点,如果满足条件,记录当前路径
// 03.单层操作,递归左右孩子节点
public void assist(TreeNode currNode, int currTarget, List<Integer> path) {
// 当前节点为叶子节点,如果满足条件,记录当前路径
if ((currNode.left == null) && (currNode.right == null)) {
// 如果符合条件,记录当前路径
if (currNode.val == currTarget) {
paths.add(new LinkedList<>(path));
}
return;
}
// 单层操作,当前节点为非叶子节点,递归非空的孩子节点,需要判断非空
if (currNode.left != null) {
path.add(currNode.left.val);
assist(currNode.left, currTarget - currNode.val, path);
path.remove(path.size() - 1); // 注意回溯
}
if (currNode.right != null) {
path.add(currNode.right.val);
assist(currNode.right, currTarget - currNode.val, path);
path.remove(path.size() - 1); // 注意回溯
}
}
迭代法,使用辅助栈记录多个内容,泛型设置为 Object,注意先存后取和获取后强制转换;
// 迭代法,DFS的统一框架,层序遍历的框架也可以,输出所有路径总和等于目标
// 辅助栈需要记录:当前节点、当前节点对应的路径数值列表、当前目标值
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> paths = new LinkedList<>();
if (root == null) {
return paths;
}
Deque<Object> stack = new LinkedList<>();
List<Integer> path = new LinkedList<>();
path.add(root.val);
stack.push(root);
stack.push(path);
stack.push(targetSum);
while (!stack.isEmpty()) {
// 注意先存后取
Integer currTarget = (Integer) stack.pop();
List<Integer> currPath = (LinkedList<Integer>) stack.pop();
TreeNode currNode = (TreeNode) stack.pop();
if (currNode != null) { // 按顺序入栈,前序中左右,入栈右左中
if (currNode.right != null) {
stack.push(currNode.right);
currPath.add(currNode.right.val);
stack.push(new LinkedList<>(currPath));
currPath.remove(currPath.size() - 1); // 回溯
stack.push(currTarget - currNode.val);
}
if (currNode.left != null) {
stack.push(currNode.left);
currPath.add(currNode.left.val);
stack.push(new LinkedList<>(currPath));
currPath.remove(currPath.size() - 1); // 回溯
stack.push(currTarget - currNode.val);
}
stack.push(currNode);
stack.push(currPath);
stack.push(currTarget);
stack.push(null);
stack.push(null);
stack.push(null);
} else {
currTarget = (Integer) stack.pop();
currPath = (LinkedList<Integer>) stack.pop();
currNode = (TreeNode) stack.pop();
if ((currNode.val == currTarget) && (currNode.left == null) && (currNode.right == null)) {
paths.add(currPath);
}
}
}
return paths;
}
07.二叉树的最近公共祖先
最近公共祖先的定义为:对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先),即最接近节点p、q;
练习题:
后序遍历的框架,先递归左右孩子,根据左右孩子递归值的情况,确定当前节点的返回值;
递归法,推荐的方式,利用后序遍历与回溯过程一致,利用回溯向上返回递归值,这个递归值就是最近公共祖先节点;
// 递归法,推荐的方式,基于后续遍历的框架,自底向上的过程与回溯一致
// 01.确定输入参数:当前节点、目标节点 和 返回值:最近公共祖先节点
// 02.终止条件:当前节点为空(到底部)或为目标节点,开始回溯,返回 当前节点
// 03.单层操作,当前节点一定非空,先递归左右孩子节点,再根据递归值进行返回
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if ((root == null) || (root == p) || (root == q)) {
return root;
}
// 单层操作,后序遍历,左右中
TreeNode leftSubTree = lowestCommonAncestor(root.left, p, q);
TreeNode rightSubTree = lowestCommonAncestor(root.right, p, q);
// 中,处理操作
if ((leftSubTree == null) && (rightSubTree == null)) {
// 如果当前节点的左右子树都不存在目标节点,则返回 null
return null;
} else if ((leftSubTree != null) && (rightSubTree == null)) {
// 如果目标节点在左子树,则返回左子树的递归值
return leftSubTree;
} else if ((leftSubTree == null) && (rightSubTree != null)) {
// 如果目标节点在右子树,则返回右子树的递归值
return rightSubTree;
} else {
// 如果目标节点分别在左子树和右子树,则当前节点就是最近公共祖先节点,自下向上的第一个公共节点
return root;
}
}