[树遍历的应用]110. 平衡二叉树(后序遍历)111.二叉树的最小深度(前序遍历)114. 二叉树展开为链表(前序遍历:迭代、morris)

12 篇文章 0 订阅
8 篇文章 0 订阅

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.二叉树的最小深度(前序遍历)

题目链接: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)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值