day14 二叉树(基础、遍历)

文章详细介绍了二叉树的前序、中序、后序遍历的递归和迭代实现方法,包括如何使用栈来辅助遍历,以及在不同遍历顺序中处理节点的逻辑。同时,强调了后序遍历的特殊性,以及在二叉树问题解决中的重要性。文章还提出了两种解题思路:遍历和分解问题,并讨论了递归遍历函数的返回值在不同情况下的处理方式。
摘要由CSDN通过智能技术生成

理论基础

种类:满二叉树、完全二叉树、二叉搜索树、平衡二叉搜索树
存储方式:链表和数组
遍历方式:深度优先遍历(前中后序遍历)、广度优先遍历(层次遍历)

递归遍历

递归算法怎么写:

  1. 确定递归函数的参数和返回值;
  2. 确定终止条件;
  3. 确定单层递归的逻辑。

递归的实现:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数。

144. 二叉树的前序遍历

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        preorder(root,res);
        return res;
    }

    public void preorder(TreeNode cur, List<Integer> res){
        if(cur == null) return ;
        res.add(cur.val);
        preorder(cur.left,res);
        preorder(cur.right,res);
    }
}

94. 二叉树的中序遍历

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        inorder(root,res);
        return res;
    }
    public void inorder(TreeNode cur, List<Integer> res){
        if(cur == null) return ;
        inorder(cur.left,res);
        res.add(cur.val);
        inorder(cur.right,res);
    }
}

145. 二叉树的后序遍历

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        postOrder(root,res);
        return res;
    }

    public void postOrder(TreeNode cur, List<Integer> res){
        if(cur == null) return;
        postOrder(cur.left,res);
        postOrder(cur.right,res);
        res.add(cur.val);
    }

}

迭代遍历

144. 二叉树的前序遍历

遍历节点的顺序和处理节点(将元素放进结果集)的顺序一致,注意左右节点入栈时,得先进右再进左,才能达到中左右的遍历效果。这是因为栈是先入后出的。

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        
        List<Integer> res = new ArrayList<Integer>();
        if(root ==  null)   return res;
        Deque<TreeNode> stack = new ArrayDeque<>();
        stack.offerFirst(root);
        while(!stack.isEmpty()){
            TreeNode cur = stack.pollFirst();
            res.add(cur.val);
            if(cur.right!=null) stack.offerFirst(cur.right);
            if(cur.left!=null)  stack.offerFirst(cur.left);
        }
        return res;
    }
}

94. 二叉树的中序遍历

遍历节点的顺序和处理节点的顺序不一致,因此利用指针来遍历节点,利用栈来记录遍历过的节点。

  • 当前节点若不为空则一直向左子节点遍历,直到为空
  • 知道指针所对应的结点为空,则证明此时栈顶结点没有左孩子,将该栈顶结点弹出并用集合保存,再将指针指向该弹出结点的右孩子
  • 重复上述过程,当指针指向空时则证明该子树遍历结束,若此时栈中没有元素可以弹出,则证明整颗树遍历完毕
class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        TreeNode cur = root;
        Deque<TreeNode> stack = new ArrayDeque<>();
        while(cur!=null || !stack.isEmpty()){
            if(cur!=null){
                stack.offerFirst(cur);
                cur = cur.left;
            }else {
                cur = stack.pollFirst();
                res.add(cur.val);
                cur = cur.right;
            }
        }
        return res;
    }

}

145. 二叉树的后序遍历

解法一:利用二叉树的前序遍历
二叉树的前序遍历是中左右,先换成中右左,再反转结果数组,得到左右中的遍历顺序,即后序遍历。

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if(root == null)    return res;
        Deque<TreeNode> stack = new ArrayDeque<>();
        stack.offerFirst(root);
        while(!stack.isEmpty()){
            TreeNode cur = stack.pollFirst();
            res.add(cur.val);
            if(cur.left!=null)  stack.offerFirst(cur.left);
            if(cur.right!=null) stack.offerFirst(cur.right);
        }
        //Collections.reverse(res);
        for(int i = 0, j = res.size() - 1; i < j; i++ , j--){
            int tmp =res.get(j);
            res.set(j,res.get(i));
            res.set(i,tmp);
        }        
        return res;
    }
}

解法二:模拟栈
遍历节点的顺序和处理节点的顺序不一致,因此利用指针来遍历节点,利用栈来记录遍历过的节点。

  • 当前节点若不为空则一直向左子节点遍历,直到为空
  • 知道指针所对应的结点为空,则证明此时栈顶结点没有左孩子,将该栈顶结点弹出
    • 若该栈顶节点是第一次出栈,则判断其右子树是否为空,若为空,则直接写入集合(左右中),若不为空,则将其重新入栈,先处理其右子树
    • 若该栈顶节点是第二次出栈,说明其左右子树都已遍历完成,则直接写入集合(左右中)
  • 通过标记的方法来判断栈顶节点是否二次出栈
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        if(root == null)    return res;
        Deque<TreeNode> stack = new ArrayDeque<>();
        TreeNode cur = root;
        //记录前一个处理的节点
        TreeNode prev = null;
        while(cur!=null || !stack.isEmpty()){
            if(cur!=null){
                //遍历到最左节点
                stack.offerFirst(cur);
                cur = cur.left;
            }else{
                cur = stack.pollFirst();
                //cur.right == null :当前节点为子树根节点
                //cur.right == prev : 右子树是上一次处理的节点则处理根节点
                if(cur.right ==null || cur.right == prev){
                    res.add(cur.val); 
                    //记录上一次处理的节点
                    prev = cur;
                    //这一步很重要,置空保证左子树不会被二次遍历。因为处理当前节点后,要向上走了
                    cur = null;
                }else{
                    // 重新把根节点入栈,处理完右子树还要回来处理根节点
                    stack.offerFirst(cur);
                    cur = cur.right;                
                }
            }
        }
        return res;
    }
}

统一迭代

  • 类似于递归的思想,比如中序遍历,就是先遍历左子树,然后处理根节点,再遍历右子树;
  • 为了解决遍历与处理顺序不一致的问题,这里将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
  • 标记方法:要处理的节点(访问过)放入栈之后,紧接着放入一个空指针作为标记。
//以后序遍历为例,注意入栈时顺序跟遍历顺序反着(左右中->中左右)
class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();     
        if(root != null)    stack.push(root);
        while(!stack.isEmpty()){
            TreeNode cur = stack.pop();
            if(cur != null){
                stack.push(cur);
                stack.push(null);
                if(cur.right != null)   stack.push(cur.right);
                if(cur.left != null)    stack.push(cur.left);
            }else{
                res.add(stack.pop().val);
            }
        }
        return res;
    }
}

新的理解

二叉树纲领

深入理解前中后序

  1. 二叉树的前中后序遍历是什么,仅仅是三个顺序不同的 List 吗?
    前序位置,就是刚进入一个节点(元素)的时候,后序位置就是即将离开一个节点(元素)的时候,把代码写在不同位置,代码执行的时机也不同。(e.g. 倒序打印链表
    在这里插入图片描述
    前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点。
  • 前序位置的代码在刚刚进入一个二叉树节点的时候执行;
  • 后序位置的代码在将要离开一个二叉树节点的时候执行;
  • 中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。
void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    // 前序位置
    traverse(root.left);
    // 中序位置
    traverse(root.right);
    // 后序位置
}

在这里插入图片描述
二叉树的所有问题,就是让你在前中后序位置注入巧妙的代码逻辑,去达到自己的目的,你只需要单独思考每一个节点应该做什么,其他的不用你管,抛给二叉树遍历框架,递归会在所有节点上做相同的操作。
2. 后序遍历有什么特殊之处?

  • 前序位置的代码执行是自顶向下的,只能从函数参数中获取父节点传递来的数据;(如何打印出每一个节点所在的层数?)
  • 中序位置(左中右)主要用在 BST 场景,BST 的中序遍历可以认为是遍历有序数组;
  • 后序位置的代码执行是自底向上的,不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。(如何打印出每个节点的左右子树各有多少节点?)

只有后序位置才能通过返回值获取子树的信息。遇到子树问题 ,首先想给函数设置返回值,然后在后序位置做文章(e.g. 二叉树的直径
3. 为什么多叉树没有中序遍历?
二叉树的每个节点只会进行唯一一次左子树切换右子树,而多叉树节点可能有很多子节点,会多次切换子树去遍历,所以多叉树节点没有「唯一」的中序遍历位置

两种解题思路

二叉树解题的思维模式分两类 (e.g. 二叉树的最大深度,前中后序遍历):

  • 【遍历】:是否可以通过遍历一遍二叉树得到答案。可以的话,用一个traverse函数配合外部变量来实现。对应回溯算法核心框架
  • 【分解问题】:是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案。可以的话,写出这个递归函数的定义,并充分利用这个函数的返回值。对应动态规划算法核心框架

上述两种思维方式都需要思考:二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做?其他的节点由递归函数执行相同的操作。

递归遍历的返回值

  1. 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(113.路径总和ii)
  2. 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (236. 二叉树的最近公共祖先 )
  3. 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(112. 路径总和)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值