[树遍历的应用]110. 平衡二叉树(后序遍历)111.二叉树的最小深度(前序遍历)114. 二叉树展开为链表(前序遍历:迭代、morris)
110. 平衡二叉树(后序遍历、剪枝)
题目链接:https://leetcode-cn.com/problems/balanced-binary-tree/
分类:树、递归(后序遍历)
这题是二叉树遍历的应用。递归练习题。
思路:后序遍历
根据后序遍历的特点:先左右,再根节点,所以在处理根节点时就已经拥有左右孩子的信息了,就能根据左右子树的高度判断二叉树是否平衡。
递归功能:返回当前根节点对应的二叉树的高度(包括根节点自身),规定返回-1表示出现不平衡的情况。
递归出口:
到达叶子节点时,说明当前节点高度为1,返回1;
递归主体:
获取左右子树的高度,取两者的最大值 + 1得到根节点对应的二叉树高度并返回。
- 如果左右子树高度差>1,则说明左右子树不平衡,返回-1;
- 如果左右子树调用的递归函数返回值是-1,说明左右子树内部发生了不平衡情况,所以也直接返回-1;(剪枝操作,避免再做无用的计算)
实现时遇到的问题:特殊用例[]
特殊用例[],空二叉树也是平衡二叉树,所以增加一个递归出口:
- 如果root == null,返回0,因为规定返回-1才是不平衡,返回0不影响最终返回的高度值,也不会影响平衡的判断,还可以顺便解决左右孩子 == null的情况。如果二叉树是空二叉树,返回0对应返回true也符合定义。
实现代码
class Solution {
public boolean isBalanced(TreeNode root) {
int res = getHeight(root);
if(res == -1) return false;
else return true;
}
//获取树的高度,并判断二叉树是否为平衡二叉树
public int getHeight(TreeNode root){
if(root == null) return 0;//特殊节点处理,规定返回-1才是不平衡,返回0不影响最终结果
//如果到达叶子节点,返回1
if(root.left == null && root.right == null) return 1;
else{
int left = getHeight(root.left);
int right = getHeight(root.right);
if(left == -1 || right == -1) return -1;//子树返回-1说明子树内部存在不平衡的情况,直接向上返回-1
if(Math.abs(left - right) > 1) return -1;//左右子树高度差>1,说明当前根节点的左右子树不平衡,返回-1
return Math.max(left ,right) + 1;
}
}
}
思路优化:短路或 + if上写赋值语句
使用短路或||,在左子树得到-1的结果(左子树不平衡)后,就不需要再处理右子树,直接返回-1即可。
if上也能使用赋值语句!
实现代码
class Solution {
public boolean isBalanced(TreeNode root) {
int res = getHeight(root);
if(res == -1) return false;
else return true;
}
//获取树的高度,并判断二叉树是否为平衡二叉树
public int getHeight(TreeNode root){
if(root == null) return 0;//特殊节点处理,规定返回-1才是不平衡,返回0不影响最终结果
//如果到达叶子节点,返回1
if(root.left == null && root.right == null) return 1;
else{
int left = -1, right = -1;
if((left = getHeight(root.left)) == -1 || (right = getHeight(root.right)) == -1 || Math.abs(left - right) > 1) return -1;
return Math.max(left ,right) + 1;
}
}
}
111.二叉树的最小深度(前序遍历)
分类:树、递归(前序遍历)
思路:前序遍历
利用前序遍历的特点,先根后左右,所以可以先判断当前根节点是否有左右孩子:
- 如果左右孩子都为空,说明当前根节点是叶子节点,则直接返回1,表示以它为根节点的树的最小深度(根节点也算在内)=1;
- 如果左右孩子不全为空,说明当前根节点不是叶子节点,就取不为空的那棵子树的最小深度 + 1返回;
- 如果左右孩子都不为空,则取min{左子树最小深度,右子树最小深度}+1返回。
递归函数功能:返回以当前root为根节点的树的最小深度
递归出口:
- if(root == null) return 0;//这一步只是为了处理特例空二叉树,根节点进入各自的左右子树前都要先判空,不能留到这一步再判空,因为会返回0,影响最小深度的计算。
- if(root.left == null && root.right == null) return 1;//说明root是叶子节点,返回1即可
递归主体:
- 如果左右子树都不为空,则获取当前根节点的左右子树最小深度(分别向左右子树调用递归函数),返回其中的较小值 + 1(这里的1就是把根节点也记上)。
实现遇到的问题:
1、最小深度的定义理解错误(易错点)
最小深度要求是根节点到叶子节点:
例如:
3
\
2
一开始我认为这棵树的最小深度是1,因为认为3->null是一条路径。但实际上最小深度是2,而不是1。因为2才是叶子节点,最小深度=3->2, 当一个节点不是叶子节点时,就不能作为最小深度路径的终点。
所以代码要做相应的修改:
(1)递归出口:root如果为null,返回0。这一步只是用于处理空二叉树的情况,而不是用于当前节点取左右孩子之后再来这里判空,因为如果其中一个孩子为空会返回0,但另一个孩子不为空,说明该节点不是叶子节点,按设计的算法是取左右子树最小深度的min返回,这里就必然会取0,相当于把该节点作为路径的终点,但我们知道非叶子节点是不能作为路径终点的,所以会出错。
因此递归函数开头部分对root的判空只是为了处理空二叉树,递归主体中每个节点的左右孩子调用递归函数前都要做一次判空,避免root=null进入递归函数。
(2)左右子树返回的最小深度,如果其中一棵子树为空,另一个不为空,则直接取不为空的子树的最小深度作为返回值;如果两个都不为空,才取两个子树最小深度的min。为了代码能够统一,在得到left,right前,先对它们的初值设置为int最大值,如果出现子树为空的情况,就保持初值,如果子树不为空,则更新为实际的最小深度,这样能确保两个子树无论什么情况下都有各自的最小深度值,而在决定最终返回值时,子树为空的最小深度也不会影响到最终结果。
实现代码:
class Solution {
public int minDepth(TreeNode root) {
if(root == null) return 0;
if(root.left == null && root.right == null) return 1;//叶子节点才返回1
else{
int left = Integer.MAX_VALUE, right = Integer.MAX_VALUE;
if(root.left != null) left = minDepth(root.left);
if(root.right != null) right = minDepth(root.right);
return Math.min(left, right) + 1;
}
}
}
114. 二叉树展开为链表(前序遍历:迭代实现、morris)
题目链接:https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/
分类:树(线索二叉树:morris遍历)、栈(前序遍历迭代实现)
题目分析:前序遍历的应用
将二叉树原地展开为单链表,也就是把二叉树写成一个按前序遍历顺序排列的链表。
思路1:前序遍历迭代实现 + 栈保存右孩子
算法设计
因为题目没有提供单独的链表节点类,所以就需要把树节点改造为链表节点,即把right域当做链表节点的next域,left域清空。
利用前序遍历的特点,在遍历过程中,把当前遍历节点的右孩子备份,然后将节点的左孩子赋给右孩子,节点取右孩子root = root.right继续遍历。
当root.right == null时,说明到达原二叉树的最左节点,开始返回,取出栈中备份的右孩子,令root = 之前备份的右子树,重复上面的操作。
前序遍历的迭代实现会使用的栈来保存每个节点的右孩子,刚好这一题也需要备份右孩子,所以栈的处理可以直接沿用前序遍历迭代实现中栈的处理。
算法流程
整体流程和前序遍历的迭代实现类似,增加了把树节点改造为链表节点的步骤。
首先,开辟一个栈保存右孩子,同时开辟一个pre指向当前遍历节点的前一个节点。
开始遍历,前序遍历到每个节点root时,先将右孩子入栈(相当于备份),然后取root.left覆盖root.right,最后清空left域,因为left域已经转移到right域上了,所以前序遍历里寻找最左节点的迭代关系式从原来的root = root.left变为root = root.right,pre也更新为root更新前的节点。
接着,在退出内层while循环时root = null,为了保持链表的连接不被中断,接下来要取最左节点和栈顶的右孩子链接,所以需要一个pre指向root的前一个节点,当root == null退出循环时,pre就指向最左节点,pre.right = 栈顶右孩子,就能保持链表的链接不中断。
最后,令root = pre.right,继续后面的前序遍历。
例如:
1
/ \
2 5
/ \ \
3 4 6
root=1,备份5,1->2,left置null
root=2,备份4,2->3,left置null
root=3,左右子树都为空,所以不需要其他处理,取出备份的4,3->4,令root=4,
root=4,左右子树都为空,所以不需要其他处理,取出备份的5,4->5,令root=5
root=5,因为左子树为空,所以直接保留右子树,令root=6
root=6,因为左右子树为空,且栈中也没有剩余的备份节点了,所以算法结束。
需要栈保存右孩子,同时前序遍历的迭代实现也需要用栈保存右孩子,所以可以共用同一个栈。
知识点:Deque代替Stack
java推荐用Deque stack = new LinkedList()作为栈使用,
Deque也提供了push,pop,peek等方法。
实现代码
class Solution {
public void flatten(TreeNode root) {
if(root == null) return;
Deque<TreeNode> stack = new LinkedList<>();
TreeNode pre = root;//指向root的前一个节点
//前序遍历的迭代实现
while(!stack.isEmpty() || root != null){
//不断遍历到最左节点,备份每一层的右孩子
while(root != null){
if(root.right != null) stack.push(root.right);
root.right = root.left;//用left域覆盖right域
root.left = null;//清空left域
pre = root;//更新pre和root
root = root.right;//left域已经转移到right域上了
}
//退出上面的循环时,root==null,pre指向最左节点,且pre.left=null
if(!stack.isEmpty()) pre.right = stack.pop();
root = pre.right;
}
}
}
思路2:morris前序遍历
morris遍历的思路就是将树改造成线索二叉树,将空闲的指针域利用起来指向遍历的后继节点,这样遍历的时候就不需要栈。本题的morris遍历和一般的版本有所不同,right域在修改后不需要恢复原状,新增的right域可以直接用来作为链表节点的next域,而left域在处理后需要置null。
1、如何构造前序遍历的线索二叉树?
前序遍历是根->左->右,下图就是一个前序遍历线索二叉树的例子:
以root=1为例,它的左子树最右节点是4,4的前序遍历后继节点是5(root.right),所以在找到左子树最右节点4时,置4.right=root.right,相当于把前序遍历的一对前驱后继节点链接起来。
构造过程:设当前节点root,在root的左子树找到最右节点,该节点的right域指向root.right。这样就完成一个节点的处理,其他节点以此类推。
在需要像思路1一样用栈弹出右孩子(后继节点)时,直接获取当前节点的right就能找到后继节点。
2、构造完线索二叉树如何再改造成链表?
线索二叉树对原本right域为空的节点做了填充,而right域刚好可以用来作为链表的next域,剩下的right域不为空的节点只需要把每个节点按思路1的方法改造成链表节点即可:
root.right = root.left;
root.left = null;
然后进入右子树(即原二叉树的左子树)继续前序遍历即可。
实现代码
class Solution {
public void flatten(TreeNode root) {
if(root == null) return;
//morris前序遍历
while(root != null){
//如果当前节点没有左孩子,则取右孩子
if(root.left == null) root = root.right;
else{
TreeNode pre = root.left;//用于存放左子树最右节点,从root的左孩子开始寻找
//寻找当前节点的左子树最右节点
while(pre.right != null){
pre = pre.right;
}
pre.right = root.right;//前驱的right域指向后继,后继节点就是root.right
//构造结束后开始处理root节点,将其改造成链表节点
root.right = root.left;
root.left = null;
root = root.right;//root进入右子树(即原二叉树的左子树)
}
}
}
}
- 空间复杂度:O(1)