目录
一,前言
在上一篇博客中,博主详细讲解了二叉树的相关概念和基本操作,为了使我们能够更加深入了解二叉树的结构和使用逻辑,故本文精选了一些具有代表性的OJ题进行讲解。
在二叉树章节中,由于代码逻辑比较抽象,文字和静态图片无法直观地表达解题思路和执行流程,故博主在本篇博客中会尝试制作动图,希望能够提升大家的理解程度。
二,拓展题
1,二叉树前序迭代遍历
题目

迭代遍历二叉树最核心的逻辑为"使用栈保存已经访问过、但右子树还未处理的节点",如此便可以记录回溯路径。在前序迭代遍历二叉树时,主要根据遍历变量cur节点的情况做以下操作:
- cur不为空:
入栈并访问当前节点(在该题中访问形式为将节点入返回链表),并使 cur = cur.left 走向左子树(此处完成根 => 左)
- cur为空:
出栈栈顶元素top,并使 cur = top.right 走向右子树(cur为空证明当前分支走到尽头,而栈顶保存的是上一个已经访问过、但右子树还未处理的节点,让 cur = top.right 即是继根 => 左之后走向右,完成前序遍历)
当cur为空且栈为空,则证明该二叉树遍历完成,对应代码如下:
java
while (cur != null || !stack.isEmpty()) {
...
}
完整代码逻辑如下图所示:

注:
二叉树的前序迭代遍历和中序迭代遍历代码几乎一样,区别在于访问节点的时机不同。前序遍历是"入栈即访问",而中序遍历是"出栈再访问",两者都是"一路向左,左空回溯转右"。伪代码如下所示:
java// 前序:入栈时访问 if (cur != null) { retList.add(cur.val); // 访问 stack.push(cur); cur = cur.left; } else { TreeNode top = stack.pop(); cur = top.right; } // 中序:出栈时再访问 if (cur != null) { stack.push(cur); cur = cur.left; } else { TreeNode top = stack.pop(); retList.add(cur.val); // 访问 cur = top.right; }
完整解题代码
java
/**
* 二叉树的前序遍历-迭代实现
* @param root 根节点
* @return {@link List }<{@link Integer }>
*/
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> retList = new LinkedList<>();
// 判空
if(root == null) {
return retList;
}
// 迭代遍历
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
while(cur != null || !stack.isEmpty()) {
if(cur != null) {
// -当前节点不为空,则入栈向左并在此处打印,此时为根=>左=>右顺序
stack.push(cur);
retList.add(cur.val);
cur = cur.left;
} else {
// -当前节点为空,则出栈向右
TreeNode top = stack.pop();
cur = top.right;
}
}
// 返回值
return retList;
}
2,二叉树后序迭代遍历
题目

后序遍历顺序为左右根,由于需要保证在右子树遍历完毕后再打印根节点,故出栈条件与前/中序有所不同,因此单独进行讲解。
在后序迭代遍历二叉树时,依旧是根据遍历变量cur节点状态分情况处理,但在此基础上还额外添加prev变量记录上一次访问(入链表)的节点:
- cur不为空:
入栈当前节点,并使 cur = cur.left 走向左子树(此处完成左 =>)
- cur为空:
先使用peek()获取栈顶元素(因为此时还不知道右子树是否遍历完毕,故不能出栈),使用top保存,再判断右子树是否访问完毕:
1,右子树访问完毕 =>
出栈栈顶元素并访问(在该题中访问形式为将节点入返回链表),同时使用prev记录该节点(此处完成 右 => 根)
注:右子树访问完毕的标志有两种:
1,top.right == null,表示右子树为空,此时无需访问
2,top.right == prev,表示右子树为上一次访问的节点,即右子树已访问完毕
上述两种情况满足任意一种即可
2,右子树未访问 =>
使 cur = top.right 走向右子树(走向右子树后依旧会按照左右根的顺序进行打印,因此可以保证右子树完成访问)
当cur为空且栈为空,则证明该二叉树遍历完成,对应代码如下:
java
while (cur != null || !stack.isEmpty()) {
...
}
完整代码逻辑如下图所示:

注:
核心逻辑为:先一路向左,左空后看栈顶;右子树为空或右子树已访问,才出栈访问根;否则转向右子树
完整解题代码
java
/**
* 二叉树的后序遍历-迭代实现
* @param root 根节点
* @return {@link List }<{@link Integer }>
*/
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> retList = new LinkedList<>();
// 判空
if(root == null) {
return retList;
}
// 迭代遍历
Stack<TreeNode> stack = new Stack<>();
TreeNode cur = root;
TreeNode prev = null;
while(cur != null || !stack.isEmpty()) {
if(cur != null) {
// -当前节点不为空,则入栈向左
stack.push(cur);
cur = cur.left;
} else {
// -当前节点为空,则判断右子树情况(不能直接出栈)
TreeNode top = stack.peek();
if(top.right == null || top.right == prev) {
// --右子树为空 或者 右子树已遍历 ==> 访问 + 记录 + 出栈
retList.add(top.val);
prev = top;
stack.pop();
} else {
// --否则 ==> 继续遍历右子树
cur = top.right;
}
}
}
// 返回值
return retList;
}
3,根据前序和中序遍历序列构建二叉树
题目

已知本题保证遍历序列正确并且本题树中无重复元素,故我们可以得出以下结论:
1,因为前序遍历的顺序是根-左-右,所以前序序列的第一个元素就是当前子树的根
2,拿到这个根值后,去中序序列中找到它,中序中它左边部分就是左子树,右边部分就是右子树
3,根据1、2逻辑再对左右子树递归重复这一过程
4,递归时存在以下三种情况:
- 当递归至子序列为空时,说明该方向没有子树,返回null(我的实现中为了减少递归次数,直接在递归前判断了子序列长度,长度小于1时直接赋值为null);
- 当子序列只有一个元素时,说明该子树是叶子节点,创建这个节点后返回;
- 当子序列长度大于一时,继续递归划分,直到变成空或单元素
5,当所有递归区间都处理完毕,整棵树构建完成,最终返回整棵树的根节点
递归构建过程如下图所示,在递推时划分序列,在回退时建立联系。

注:
对于此类二叉树递归问题,我的经验是不要去深究左右子树的递归细节,直接假定递归能够完成任务,先写出解决根节点处的解法,后续再完善细节。不过有三点需要注意:
1,需要确定递归的返回值,一般来说是我们所求的内容,以本题为例,返回根节点
2,需要确定递归的参数传递,要确保传递下去的参数能够保证下一层也能够完成任务
3,需要确定递归的终止条件,可以套用本层的情况考虑
按照此思想我将代码编写思路注释如下:
完整解题代码
java
public class Solution {
private int index = 0;
/**
* 从前序与中序遍历序列构造二叉树
* @param preorder 前序遍历序列
* @param inorder 中序遍历序列
* @return {@link TreeNode }
*/
public TreeNode buildTree(int[] preorder, int[] inorder) {
// 递归出口
if(index == preorder.length) {
return null;
}
// 创建根节点
int val = preorder[index ++];
int pivot = findIndex(inorder, val);
TreeNode root = new TreeNode(val);
// 递归左右子树
if(pivot > 0) {
root.left = buildTree(preorder, Arrays.copyOfRange(inorder, 0, pivot));
} else {
root.left = null;
}
if(inorder.length - pivot > 1) {
root.right = buildTree(preorder, Arrays.copyOfRange(inorder, pivot + 1, inorder.length));
} else {
root.right = null;
}
// 返回值
return root;
}
/**
* 查找元素并返回下标
* @param arr 目标数组
* @param t 目标值
* @return int 目标值下标
*/
private int findIndex(int[] arr, int t) {
for(int i = 0; i < arr.length; i ++) {
if(arr[i] == t) {
return i;
}
}
return -1;
}
}
4,判断一棵二叉树是否是平衡二叉树
题目

解析
所谓平衡二叉树,即是指该树所有节点的左右子树高度相差不超过1。
由上述概念可得,我们若想判断一棵树是否是平衡二叉树,则需要满足"左右子树均为平衡二叉树,并且左右子树的高度相差不超过1"。
由上述描述我们不难写出解题代码,伪代码如下:
java
public boolean function(TreeNode node) {
// 判空
if(node == null) {
return true;
}
// 先判断当前树是否是平衡二叉树
int left = getTreeHigh(node.left);
int right = getTreeHigh(node.right);
if(Math.abs(left - right) > 1) {
return false;
}
// 再递归判断左右子树是否是平衡二叉树
return function(node.left) && function(node.right);
}
private int getTreeHigh(TreeNode node) {
// 判空
if(node == null) {
return 0;
}
// 获取左右子树高度取最大值 + 1返回
int left = getTreeHigh(node.left);
int right = getTreeHigh(node.right);
return Math.max(left, right) + 1;
}
以上伪代码思想很直接,并且递归很简单,同时我们不难发现:在function()方法中调用getTreeHigh()方法获取左右子树高度求高度差值以判断当前树是否是平衡二叉树,而在getTreeHigh()内部也调用了getTreeHigh()方法获取左右子树高度求左右子树最大高度,故我们不禁生出这样的疑问------是否可以在获取树的高度时就判断其是否是平衡二叉树呢?
答案是可以的!由于在getTreeHigh()方法中我们就会获取左右子树高度,故当左右子树差值高度超过1时就直接返回-1,并且在递归中对-1进行拦截,只要返回-1就直接返回,那么就可以实现对二叉树的判定。
完整解题代码
java
/**
* 判断一棵二叉树是否是平衡二叉树
* @param root 根节点
* @return boolean
*/
public boolean isBalanced(TreeNode root) {
// 直接调用getMaxDepth()方法
return getMaxDepth(root) != -1;
}
/**
* 获取二叉树的最大深度
* @param root 根节点
* @return int
*/
private int getMaxDepth(TreeNode root) {
// 判空
if(root == null) {
return 0;
}
// 获取左右子树高度,并拦截-1
int leftDepth = getMaxDepth(root.left);
if(leftDepth == -1) {
return -1;
}
int rightDepth = getMaxDepth(root.right);
if(rightDepth == -1) {
return -1;
}
if(Math.abs(leftDepth - rightDepth) > 1) {
return -1;
}
// 当左右子树均为二叉树且当前左右子树也平衡时,返回最大深度
return Math.max(leftDepth, rightDepth) + 1;
}
5,给定一棵二叉树,找到指定两个节点的最近公共祖先
题目

由本题提示可知,指定的两个节点必定存在于树中。故易得在最差情况下,root节点为两个节点的最近公共祖先。我们若是直接上手去做可能感觉有点复杂,因为需要判断两个节点的位置情况,故本题博主使用一种较为易懂的方式进行解题。
我们可以先找到根节点到目标节点路径上所有的节点并使用栈存储起来,如此我们便可以得到两个栈,如下图示。

因为这两条路径都是从root节点开始,并且其必定存在公共祖先,则从root到最近公共祖先这一段路径一定是相同的,从最近公共祖先之后路径开始出现分歧。故将栈元素个数多的一方出栈,直至两个栈元素个数相等,再同时出栈判断节点是否相同,所遇见的第一个相同节点一定是最近公共祖先。
注:
由于路径节点按从根到目标节点的顺序入栈,因此栈顶是目标节点,栈底是根节点。出栈比较时,实际是从目标节点开始,沿着路径向上回溯,也就是从树的下方往上方比较。先让较长栈出栈至两栈等长,使两个节点处于同一深度,再同时出栈,遇到的第一个相同节点即为最近公共祖先。
那么现在我们只需要解决"找到根节点到目标节点路径上所有的节点并使用栈存储起来"这一需求即可。可以使用递归实现该需求,逻辑如下:
DFS找路径,每次递归时root存在以下三种情况:
1,root为null ==> 返回null,说明这个方向没有我们要找的节点
2,root为目标节点 ==> 将root入栈并返回栈,表明已经找到
3,root为其它节点 ==> 将root入栈并递归root的左右子树,此时存在两种情况:
3.1,递归左右子树的返回值均为null ==> 出栈栈顶元素,因为左右为空说明在左右子树中均未找到目标值,故当前根节点不为路径上的节点
3.2,递归左右子树的返回值不是均为null ==> 返回栈,因为左右子树的返回值有一个不为空,则说明已经在左/右子树找到目标节点并记录,此时直接返回即可
java
/**
* 获取根节点到目标节点路径上的所有节点并入栈存储
* @param root 根节点
* @param t 目标节点
* @return {@link Stack }<{@link TreeNode }>
*/
private Stack<TreeNode> getPathStack(Stack<TreeNode> path, TreeNode root, TreeNode t) {
// 递归出口
if(root == null) {
return null;
}
if(root == t) {
path.push(root);
return path;
}
// 入栈
path.push(root);
// 递归左右树
Stack<TreeNode> leftStack = getPathStack(path, root.left, t);
Stack<TreeNode> rightStack = getPathStack(path, root.right, t);
// 根据左右树情况返回
if(leftStack == null && rightStack == null) {
path.pop();
return null;
}
return path;
}
注:
在递归完左/右子树后也可以判断其返回值拦截stack,这样可以避免在左子树找到后还递归右子树的情况,但为了让代码易懂,此处就不做优化
递归过程如下图所示:

完整解题代码
java
/**
* 二叉树的最近公共祖先
* 由题可知,两个节点必定存在,则必定存在公共祖先,最差情况是root
* @param root 根节点
* @param p 节点1
* @param q 节点2
* @return {@link TreeNode }
*/
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 分别获取两个节点从根节点出发的路径
Stack<TreeNode> stackP = getPathStack(new Stack<TreeNode>(), root, p);
Stack<TreeNode> stackQ = getPathStack(new Stack<TreeNode>(), root, q);
// 使两条路径节点个数相等
int len1 = stackP.size();
int len2 = stackQ.size();
if(len1 > len2) {
for(int i = 0; i < len1 - len2; i ++) {
stackP.pop();
}
} else if(len1 < len2) {
for(int i = 0; i < len2 - len1; i ++) {
stackQ.pop();
}
}
// 同时出栈比较找到相同节点(由于是使用栈存储,比较顺序为从下往上找,因此遇见的第一个相同节点一定是最近的)
while(!stackP.isEmpty()) {
if(stackP.peek() == stackQ.peek()) {
return stackP.peek();
}
stackP.pop();
stackQ.pop();
}
return null;
}
/**
* 获取根节点到目标节点路径上的所有节点并入栈存储
* @param root 根节点
* @param t 目标节点
* @return {@link Stack }<{@link TreeNode }>
*/
private Stack<TreeNode> getPathStack(Stack<TreeNode> path, TreeNode root, TreeNode t) {
// 递归出口
if(root == null) {
return null;
}
if(root == t) {
path.push(root);
return path;
}
// 入栈
path.push(root);
// 递归左右树
Stack<TreeNode> leftStack = getPathStack(path, root.left, t);
Stack<TreeNode> rightStack = getPathStack(path, root.right, t);
// 根据左右树情况返回
if(leftStack == null && rightStack == null) {
path.pop();
return null;
}
return path;
}
