二叉树这块内容还是比较多的,但是模板也比较固定,总的来说难度不算太大。
个人认为二叉树难点主要有两个
1、遍历顺序
2、递归返回值以及处理方式
根据随想录的总结思路,分成以下几个模块
1、二叉树的遍历方式
2、二叉树的属性
3、二叉树的修改与构造
4、二叉搜索树的属性
5、二叉搜索树的修改与构造
6、公共祖先问题
目前想法是,二叉树与二叉搜索树各分开写一篇,公共祖先问题另写一篇。
那么这篇就是写普通二叉树的思路总结与方法。
目录
一、二叉树的遍历方式
二叉树的遍历方式有四种,前序、中序、后序遍历以及层序遍历。
其中前三种遍历方式关键就是中节点处理顺序,递归方式和迭代方式都是一样的。
递归比较简单,只是调换中节点的处理顺序就可以了,这里只记录迭代方式。
1.1、前序遍历
遍历顺序为中左右,中节点是最先遍历的。
因为节点的遍历顺序和处理顺序是不一致的,所以要使用栈保存节点。
代码如下
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
Stack<TreeNode> s = new Stack();
ArrayList<Integer> res = new ArrayList(); // 保存结果
if (root != null) s.push(root);
while (!s.isEmpty()) {
TreeNode node = s.pop(); // 处理中节点 即根节点
res.add(node.val);
if (node.right != null) s.push(node.right); // 先放右节点 因为右节点最后出栈
if (node.left != null) s.push(node.left);
}
return res;
}
}
1.2、后序遍历
后序遍历为左右中,即中右左的反向,因此可以将前序遍历的左右孩子入栈顺序交换,再reverse一下结果集就可以了。
代码如下
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
Stack<TreeNode> s = new Stack();
ArrayList<Integer> res = new ArrayList();
if (root != null) s.push(root);
while (!s.isEmpty()) {
TreeNode node = s.pop();
res.add(node.val);
if (node.left != null) s.push(node.left); // 调换一下左右孩子入栈顺序
if (node.right != null) s.push(node.right);
}
Collections.reverse(res); // 反转结果集
return res;
}
}
1.3、中序遍历
中序遍历为左中右,与前后序的代码逻辑不同。
得先遍历所有左节点,一旦遇到空节点则当前节点为根节点,处理完后再遍历右节点。
代码如下
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
Stack<TreeNode> s = new Stack();
ArrayList<Integer> res = new ArrayList();
TreeNode node = root;
while (!s.isEmpty() || node != null) {
while (node != null) {
s.push(node);
node = node.left; // 左
}
node = s.pop();
res.add(node.val); // 中
node = node.right; // 右
}
return res;
}
}
1.4、层序遍历
层序遍历是一层层往下遍历,每一层的顺序是从左往右。
层序遍历使用队列实现,要求每处理一个节点,就将其左右节点放入,并将处理的当前节点弹出。
代码如下
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new LinkedList();
List<List<Integer>> res = new ArrayList();
if (root != null) queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size(); // 这里是每一层的节点个数
ArrayList<Integer> temp = new ArrayList();
while (size > 0) { // 处理每一层节点
TreeNode node = queue.remove(); // 当前节点
temp.add(node.val);
if (node.left != null) queue.add(node.left); // 处理完当前节点 放左右孩子进入队列
if (node.right != null) queue.add(node.right);
size--;
}
res.add(new ArrayList(temp));
}
return res;
}
}
二、二叉树的属性
2.1、二叉树是否对称
看二叉树是否对称,其实就是看左右子树是否可以相互翻转,或者说具体一点,左、右子树的外层与左、右子树的内层是否相等,如果完全相等则可以相互翻转。
那么思路就变成了同时遍历左右子树的外层和内层,如果都相等那么对称,有任何一个节点不相等则不对称。
根据递归三部曲,首先
1、确定递归的返回值和参数,因为一个节点要知道下层节点,即孩子是否是对称的,如果孩子不对称,那么无论当前节点的比较结果如何,都向上返回告知上层节点,因此递归返回值为boolean.
要同时遍历两棵树,则参数为root1和root2.
2、确定递归终止条件,本道题有两个终止条件,即当前节点为空,或者孩子不构成对称,那么可以直接向上返回
3、遍历顺序,这道题其实都可以,但优先前序遍历,因为根据终止条件,自顶向下是可以剪枝的。
代码如下
class Solution {
public boolean isSymmetric(TreeNode root) {
if (root == null) return true;
return traversal(root.left, root.right);
}
public boolean traversal(TreeNode left, TreeNode right) {
// 四种情况 已经包含了递归终止条件
// 先判断当前节点
// 都为空 返回true
if (left == null && right == null) return true;
// 只有其中一个为空 返回false
else if (left != null && right == null || right != null && left == null) return false;
// 都不为空 数值不相等的话 返回false
else if (left != null && right != null && left.val != right.val) return false;
// 都不为空 看看子树是否对称
boolean outV = traversal(left.left, right.right);
boolean inV = traversal(left.right, right.left);
return outV && inV;
}
}
2.2、深度问题
树中的深度和高度的概念:
- 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始)
- 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数后者节点数(取决于高度从0开始还是从1开始)
通俗点说,深度是从上往下数的,类似于从地面向下延伸,高度是从下往上数的,类似于从地面向上生长。
那么显然,根节点的深度为1,根节点的高度为树的层数。
理解了这些,做题就不会搞混,并且可以确定遍历顺序。
2.2.1、最大深度
对应题目104. 二叉树的最大深度 - 力扣(LeetCode)
最大深度其实就是树的高度,即从最底层的节点往上数,一直到根节点的层数。
那么遍历顺序就是后序遍历了,即自底向上。
递归三部曲:
1、递归返回值和参数,由于下层节点要告诉本层节点它的深度信息,本层节点也要向上返回本层的节点信息,因此返回值类型为int,遍历一棵树,参数为root.
2、终止条件,遇到空节点就返回深度为0
3、遍历顺序,后序遍历。
代码如下
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
int leftD = maxDepth(root.left); // 左子树的深度
int rightD = maxDepth(root.right); // 右子树的深度
return Math.max(leftD, rightD) + 1; // + 1是算上本节点的深度 向上返回
}
}
2.2.2、最小深度
对应题目111. 二叉树的最小深度 - 力扣(LeetCode)
最小深度,其实就是从根节点出发到层数最近的叶子结点的层数,注意是叶子结点。
此时可以用前序遍历,但依然可以使用后序遍历,只要获取左右节点深度的最小值就可以了。
不过这道题相比最大深度有一个坑,两次我都踩进去了,即不能像最大深度一样,直接返回左右节点深度的最小值,即
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
int leftD = minDepth(root.left);
int rightD = minDepth(root.right);
return Math.min(leftD, rightD) + 1; // 中
}
}
这样写是错误的,因为会忽略一种情况
此时如果直接返回左右孩子的深度最小值,就会直接返回1,导致错误答案。
所以还要加入约束条件,即当前节点左孩子或右孩子为空时,返回不为空的孩子的深度加上本节点的深度。
正确代码如下
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
int leftD = minDepth(root.left);
int rightD = minDepth(root.right);
// 一定要记得加上本节点的深度
if (root.left == null && root.right != null) return rightD + 1; // 左为空 右不为空
if (root.right == null && root.left != null) return leftD + 1; // 右为空 左不为空
return Math.min(leftD, rightD) + 1; // 中 处理了左右都为空或都不为空的情况
}
}
2.3、二叉树节点个数
2.3.1、普通二叉树节点个数
如果是普通二叉树的节点个数,直接递归遍历统计就行了。
递归返回值为int,因为要告知上层节点本层节点的节点个数。
遍历顺序无关紧要,哪种都可以。
代码如下
class Solution {
public int countNodes(TreeNode root) {
if (root == null) return 0;
// 返回左右孩子加上当前节点的节点个数
return countNodes(root.left) + countNodes(root.right) + 1;
}
}
2.3.2、完全二叉树节点个数
完全二叉树除了底层节点外,其余层节点都是满的。
满二叉树可以有公式计算节点个数
若满二叉树层数为h,则节点个数为2^h - 1,底层节点个数为2^(h-1)。
而满二叉树是完全二叉树的特例,那么完全二叉树中一定会有满二叉树
因此如果遍历到一个节点是满二叉树的根节点,那么可以直接用公式计算节点个数,其下面的节点就不用再遍历了,可以提高效率。
那么如何判断一棵树是满二叉树?方法就是直接遍左右子树的外层节点,看深度是否相等,如果相等则一定是满二叉树。
是满二叉树:
不是满二叉树:
代码如下
class Solution {
public int countNodes(TreeNode root) {
// 完全二叉树节点个数解法 效率更高
if (root == null) return 0;
TreeNode leftNode = root.left;
TreeNode rightNode = root.right;
int leftD = 0; // 初始化为0 方便后面指数计算
int rightD = 0;
// 当遇到满二叉树的话 可以直接通过公式把该满二叉树的节点算出来 就不用遍历它下面的孩子节点了
while (leftNode != null) {
leftD++;
leftNode = leftNode.left;
}
while (rightNode != null) {
rightD++;
rightNode = rightNode.right;
}
// 完全二叉树 左右子树外层深度相同 就是满二叉树 那么直接计算并返回
if (leftD == rightD) {
// 2 << leftD 其实就是2^(leftD + 1) 比如leftD为2 那么节点数为2^(2+1)
return (2 << leftD) - 1;
}
// 不是满二叉树 老老实实递归
return countNodes(root.left) + countNodes(root.right) + 1;
}
}
2.4、是否平衡
这道题要抓住判断一棵树是否是平衡树的两个关键点:
1、左右子树都是平衡树
2、左右子树高度差小于等于1
满足以上两点,才能是平衡树,换句话说,有一点不满足,那么就不是平衡树。
递归三部曲:
1、递归返回值和参数,由于要获知孩子的高度差信息同时也要获知孩子是否是平衡树,必须使用Int,当孩子是平衡树时,返回其高度,不是平衡树时返回-1,个人认为这个点是比较难想到的点,如果想到了就会比较好做。参数为root。
2、终止条件,遇到空节点就返回高度为0
3、遍历顺序,由于要获取左右孩子的高度来做后续处理,所以使用后序遍历,自底向上。
代码如下
class Solution {
public boolean isBalanced(TreeNode root) {
return traversal(root) != -1;
}
// 该节点为平衡树 返回高度
// 该节点不为平衡树 返回-1
public int traversal(TreeNode root) {
if (root == null) return 0;
int leftD = traversal(root.left); // 左子树高度
int rightD = traversal(root.right); // 右子树高度
// -1表示该节点为根的树不为平衡树
if (leftD == -1 || rightD == -1) return -1; // 左右子树有一个不为平衡树
if (Math.abs(leftD - rightD) > 1) return -1; // 左右子树高度差大于1
return Math.max(leftD, rightD) + 1; //该节点为根的树是平衡树 返回树的高度
}
}
2.5、回溯与路径问题
回溯其实就是在调用完函数后,使某一变量回到调用之前的状态,即该变量的信息与调用函数绑定。
而递归完后会有函数回退的过程,这就是天然的回溯过程。
从具体题目来看回溯会更清晰一些。
2.5.1、所有路径
对应题目257. 二叉树的所有路径 - 力扣(LeetCode)
这道题要搜集树的所有路径,即自顶向下搜索。
在遍历树的过程中搜集路径,一旦到了叶子节点,就收集路径到结果集。
那么搜集完一个路径后,如何回退?此时就要回溯了
// 递归回溯一定要放一起
if (root.left != null) {
traversal(root.left, path);
path.remove(path.size() - 1); // 回溯过程
}
if (root.right != null) {
traversal(root.right, path);
path.remove(path.size() - 1);
}
当调用完traversal(root.left, path)后,path路径添加了一个元素,此时要保持与调用之前一样的状态,就得将该path回退,即去除添加后的那个元素。
递归与回溯总是一起的,即递归完后一定要跟着回溯。
参数path可以作为全局变量,也可以作为参数传递。
代码如下
class Solution {
ArrayList<String> res = new ArrayList(); // 保存结果
public List<String> binaryTreePaths(TreeNode root) {
ArrayList<Integer> path = new ArrayList(); // 保存路径
traversal(root, path);
return res;
}
public void traversal(TreeNode root, ArrayList<Integer> path) {
path.add(root.val); // 中 一定要先放 遍历到一个就放一个
// 到叶子结点 收集路径
if (root.left == null && root.right == null) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < path.size() - 1; i++) {
sb.append(path.get(i)).append("->");
}
sb.append(path.get(path.size() - 1));
res.add(sb.toString());
return;
}
// 递归回溯一定要放一起
if (root.left != null) {
traversal(root.left, path);
path.remove(path.size() - 1); // 回溯过程
}
if (root.right != null) {
traversal(root.right, path);
path.remove(path.size() - 1);
}
}
}
2.5.2、找树左下角的值
对应题目513. 找树左下角的值 - 力扣(LeetCode)
这道题的思路就是通过前序遍历找最底层的节点,一旦找到深度最大的节点,那么该节点一定是最左边的节点。
递归三部曲:
1、递归返回值,直接找深度最大的节点,不需要返回值;参数为root
2、终止条件,遇到空节点就返回
3、遍历顺序,前序遍历
记录一个全局最大深度与深度的计数值,一旦深度的计数值大于最大深度,说明找到了节点。
代码如下
class Solution {
int maxDepth = Integer.MIN_VALUE;
int res = 0;
int depth = 0;
public int findBottomLeftValue(TreeNode root) {
// 递归回溯解决 中序遍历同时记录最大深度 深度最大的一定是最左边的
traversal(root);
return res;
}
public void traversal(TreeNode node) {
if (node == null) return;
if (depth > maxDepth) {
maxDepth = depth; // 不断更新最大深度
res = node.val; // 记录该节点的值
}
depth += 1;
traversal(node.left);
depth -= 1;
depth += 1;
traversal(node.right);
depth -= 1;
}
}
其实这样写代码有些冗余,完全可以将depth作为参数进行传递,即回溯的另一种写法
class Solution {
int maxDepth = Integer.MIN_VALUE;
int res = 0;
public int findBottomLeftValue(TreeNode root) {
// 递归回溯解决 中序遍历同时记录最大深度 深度最大的一定是最左边的
traversal(root, 0);
return res;
}
public void traversal(TreeNode node, int depth) {
if (node == null) return;
if (depth > maxDepth) {
maxDepth = depth; // 不断更新最大深度
res = node.val; // 记录该节点的值
}
traversal(node.left, depth + 1); // 包含回溯过程
traversal(node.right, depth + 1);
}
}
每次传入的depth + 1,由于作为形参传递,并不会影响depth本身的值,所以每次回到该函数,depth的值是不变的,相当于回溯过程了。
2.5.3、路径总和
这道题其实和所有路径很像,都是到叶子结点进行处理。
这道题思路是遍历时累加节点数值,到叶子结点判断是否和目标值相等。
递归三部曲:
1、递归返回值类型和参数,要告诉上层节点,该节点的叶子结点是否包含路径总和,只要左右孩子有一个满足条件,向上返回true。因此返回值为boolean类型。参数为root,targetSum,res。累加数值res直接作为参数进行传递和回溯。
2、终止条件,遇到叶子就直接判断是否等于目标和
3、遍历顺序,要处理左右孩子的返回结果,后序遍历
代码如下
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
return traversal(root, targetSum, root.val);
}
public boolean traversal(TreeNode root, int targetSum, int res) {
if (root.left == null && root.right == null) {
return res == targetSum; // 到叶子 判断和是否相等
}
boolean leftR = false;
boolean rightR = false;
if (root.left != null) { // 左
leftR = traversal(root.left, targetSum, res + root.left.val); // 回溯
}
if (root.right != null) { // 右
rightR = traversal(root.right, targetSum, res + root.right.val); // 回溯
}
return leftR || rightR; // 中 左右子树返回结果只要有一个为true 表示找到了路径 向上返回true
}
}
三、二叉树的修改与构造
这部分二叉树的修改,包含三个部分:
1、二叉树翻转
2、二叉树构造
3、二叉树合并
3.1、二叉树翻转
这道题其实思路比较好想到,就是直接反转左右孩子,最后达到整棵树反转的目的。
比较简单,递归不需要返回值,遍历顺序可以选择前序、后序或者层序。
但不要用中序,因为中序遍历是左中右,因为反转中节点之后,原本的右子树变成了未反转中节点前的左子树,而原先未反转中节点的左子树已经反转过了,不能再反转,因此逻辑上实现太别扭。
递归实现比较简单,给出迭代版的代码
层序遍历:
class Solution {
public TreeNode invertTree(TreeNode root) {
// 可以前 后序 唯独不能中序
// 中序的话 遍历的右子树实际上是原来的左子树 而左子树已经反转过了 所以要反转两次左子树
Stack<TreeNode> s = new Stack();
if (root != null) s.push(root);
while (!s.isEmpty()) {
TreeNode node = s.pop();
swap(node); // 交换左右孩子
if (node.right != null) s.push(node.right);
if (node.left != null) s.push(node.left);
}
return root;
}
// 交换左右孩子
public void swap(TreeNode node) {
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
}
}
3.2、二叉树构造
对应题目106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)
这种题目比较死板,直接记住步骤就可以做
1、以后序数组的最后一个元素作为根节点,如果是前序数组就找第一个元素,之后找该元素在中序数组的下标index作为分割点
2、以分割点创建根节点
3、对中序数组进行分割,分成左右子数组(不包括已经创建的节点元素),注意数组的边界,一般左闭右开。
4、对后序数组进行分割,后序左数组的右边界等于后序左边界 + (中序左数组的个数 + 1),后序右数组就取剩下的元素(不包括已经创建的节点元素)
5、递归创建左子树和右子树
递归三部曲:
1、递归返回值和参数,要获取左右子树,因此返回值为TreeNode,参数为中序和后序数组,以及其左右数组的边界,以边界作为参数进行子树的构建。
2、终止条件,①当数组边界出现异常,即越界时,返回空节点;②只剩一个元素,确定是叶子节点,直接返回当前节点
3、遍历顺序,前序遍历。
代码如下
class Solution {
// 需要map辅助定位中序数组的下标
HashMap<Integer, Integer> map = new HashMap();
public TreeNode buildTree(int[] inorder, int[] postorder) {
// 映射下标
for (int i = 0; i < inorder.length; i++) map.put(inorder[i], i);
// 左闭右开
return traversal(inorder, 0, inorder.length, postorder, 0, postorder.length);
}
public TreeNode traversal(int[] inorder, int inorderBegin, int inorderEnd, int[] postorder, int postorderBegin, int postorderEnd) {
// 判断是否越界
if (postorderEnd == postorderBegin) return null;
// 创建当前节点
TreeNode root = new TreeNode(postorder[postorderEnd - 1]); // 中逻辑
// 判断是否为叶子 是叶子直接返回当前节点
if (postorderEnd - postorderBegin == 1) return root;
// 寻找分割点
int delimiterIndex = map.get(postorder[postorderEnd - 1]);
// 分割中序数组和后序数组 左闭右开
// 分割中序数组
int leftInorderBegin = inorderBegin;
int leftInorderEnd = delimiterIndex;
int rightInorderBegin = delimiterIndex + 1; // 这个节点选过了 不能再选
int rightInorderEnd = inorderEnd;
// 分割后序数组
int leftPostorderBegin = postorderBegin;
int leftPostorderEnd = postorderBegin + leftInorderEnd - leftInorderBegin;
int rightPostorderBegin = leftPostorderEnd;
int rightPostorderEnd = postorderEnd - 1; // 这个节点选过了 不能再选
// 递归构建左右子树
root.left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder, leftPostorderBegin, leftPostorderEnd); // 左
root.right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd); // 右
return root;
}
}
3.3、二叉树合并
这道题也比较简单,主要是分三种情况。
1、两棵树的节点都为空,直接返回空。
2、两棵树节点有一个为空,返回不为空的节点。
3、都不为空,数值相加并返回节点。
递归三部曲:
1、递归返回值和参数,要获取左右子树,因此返回值为TreeNode,要遍历两棵树,参数为root1,root2
2、终止条件,三种情况其实都可以作为终止条件了。
3、遍历顺序,中的处理逻辑不需要孩子的返回值来做后续处理,因此前序和后序都行。
代码如下
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if (root1 == null) return root2; // 有一个为空 返回另一个 包含了两个都为空的情况
if (root2 == null) return root1;
root1.val += root2.val; // 中逻辑 两个都不为空的情况
root1.left = mergeTrees(root1.left, root2.left); // 左
root1.right = mergeTrees(root1.right, root2.right); // 右
return root1;
}
}