首页 > 编程语言 >Java实现二叉树的先序、中序、后序、层序遍历(递归+非递归方法),附带自己深入浅出的讲解

Java实现二叉树的先序、中序、后序、层序遍历(递归+非递归方法),附带自己深入浅出的讲解

时间:2022-12-12 21:35:33浏览次数:57  
标签:node 遍历 Java 递归 二叉树 null root 节点

 

二叉树(Binary tree)是树形结构的一个重要类型,也一种非常重要的数据结构,更是算法题中高频出现的知识点,不管是为了应付工作还是面试,都有必要深度学习一下。

二叉树有多种遍历方法,如:层次遍历、深度优先遍历、广度优先遍历等。

本文只涉及了二叉树的先序、中序、后序和层序的递归和非递归遍历。莫贪多,学好这四种就完全够用了。

概念
看到网上诸多相关文章都很高大上,内容确实很丰富,但是总觉得对初学者不太友好。

我在初学二叉树的时候,就总是看不懂其中的门道,感觉自己是理解了,过几天算法题还是不会解决。后来花费了很大的精力才总结出一些规律,不一定适合所有人,但是想分享出来供大家参考。

所以,在学习代码之前,有必要先来了解一下先序、中序、后序和层序的概念。

1. 先序、中序、后序遍历关系:
先序遍历(根左右):

考察到一个节点后,立刻输出该节点的值,然后继续遍历其左右子树;
根在前,从左往右,一棵树的根永远在左子树前面,左子树又永远在右子树前面;
核心:先输出自己,再遍历子树。
中序遍历(左根右):

考察到一个节点后,将其暂存,遍历完左子树后,再输出该节点的值,然后遍历右子树;
根在中,从左往右,一棵树的左子树永远在根前面,根永远在右子树前面;
核心:自己的左子没有数据,才将自己输出。
后序遍历(左右根):

考察到一个节点后,将其暂存,遍历完左右子树后,再输出该节点的值;
根在后,从左往右,一棵树的左子树永远在右子树前面,右子树永远在根前面;
核心:自己的左子和右子都没有数据,才将自己输出。

 

 

 

 


上图中的二叉树,分别用三种遍历方法得到的结果:

  • 先序:1-2-4-6-7-8-3-5
  • 中序:4-7-6-8-2-1-3-5
  • 后序:7-8-6-4-2-5-3-1

观察:

综合上述结果,对照概念,结合图例中的白线,仔细研究一下:

不管哪种遍历方法,考察节点的顺序都是沿着白线方向走的,输出的结果也都是沿着白线顺序输出的。其中,考察节点的顺序就是二叉树的遍历方向。

结论:

无论是哪种遍历方法,考察节点的顺序都是一样的(留意图中的白线方向);
只不过考察了节点后,有时候是直接输出(先序),有时候是将其暂存(中序、后序),需要在之后的过程中输出。

 

2. 层序
层序比较好理解,参照上图中的 绿色剪头(→)方向。就是按照树的深度,从上到下,从左到右,依次输出:

层序:1-2-3-4-5-6-7-8

递归和非递归实现
如果概念理解了,那代码就跟容易理解了。

每种遍历方法列举2种实现方式:递归方式、非递归方式;

  • 递归方法:很好理解,就是按照白线方向遍历二叉树就行了,区别就是在不同的位置输出节点数据。
  • 非递归方法:因为遍历过程涉及到了节点的回溯(即:遍历完节点的左子树后,再返回节点,接着遍历节点的右子树),为了能准确的找回该节点,采用了栈结构-Stack(先进后出)对节点进行暂存。

树节点的创建:

public class TreeNode {
    int val;        
    TreeNode left;
    TreeNode right;
    // 构造方法
    public TreeNode(int val) { this.val = val; }
}


1.先序遍历
递归实现
/** —————————— 先序遍历:递归实现 —————————— */

    /** —————————— 先序遍历:递归实现 —————————— */
    public static void recursionPreorderTraversal(TreeNode root) {
        if (root != null) {
            System.out.print("输出节点:" + root .val);
            recursionPreorderTraversal(root.left);
            recursionPreorderTraversal(root.right);
        }
    }

  


非递归实现

 

 

 


为了方便理解,我把遍历过程的描述全部写在了代码注释里:

    /** —————————— 先序遍历:非递归 —————————— */
    public static void preorderTraversal(TreeNode root) {
        if(root == null)
            return;
        // 用来暂存节点的栈
        Stack<TreeNode> treeNodeStack = new Stack<TreeNode>();
        // 新建一个游标节点为根节点
        TreeNode node = root;
        // 当遍历到最后一个节点的时候,无论它的左右子树都为空,并且栈也为空
        // 所以,只要不同时满足这两点,都需要进入循环
        while (node != null || !treeNodeStack.isEmpty()) {
            // 若当前考查节点非空,则输出该节点的值
            // 由考查顺序得知,需要一直往左走
            while (node != null) {
                System.out.print("输出节点:" + node.val);
                // 为了之后能找到该节点的右子树,暂存该节点
                treeNodeStack.push(node);
                node = node.left;
            }
            // 一直到左子树为空,则开始考虑右子树
            // 如果栈已空,就不需要再考虑,弹出栈顶元素,将游标等于该节点的右子树
            if (!treeNodeStack.isEmpty()) {
                node = treeNodeStack.pop();
                node = node.right;
            }
        }
    }

 

2.中序遍历
递归实现

    /** —————————— 中序遍历:递归 —————————— */
    public static void recursionMiddleorderTraversal(TreeNode root) {
        if (root != null) {
            recursionMiddleorderTraversal(root.left);
            System.out.print("输出节点:" + root.val);
            recursionMiddleorderTraversal(root.right);
        }
    }

 

非递归实现
中序和先序的非递归遍历非常类似,唯一区别是考查到当前节点时,并不直接输出该节点。而是当考查节点为空时,从栈中弹出的时候再进行输出(永远先考虑左子树,直到左子树为空才访问根节点)。

如果过程不理解,可以参照“前序遍历”的注释描述。

    /** —————————— 中序遍历:非递归 —————————— */
    public static void middleorderTraversal(TreeNode root) {
        if(root == null){
            return;
        }
        Stack<TreeNode> treeNodeStack = new Stack<TreeNode>();
        TreeNode node = root;
        while (node != null || !treeNodeStack.isEmpty()) {
            while (node != null) {
                treeNodeStack.push(node);
                node = node.left;
            }
            if (!treeNodeStack.isEmpty()) {
                node = treeNodeStack.pop();
                System.out.print("输出节点:" + node.val);
                node = node.right;
            }
        }
    }

 

3.后序遍历
递归实现

    /** —————————— 后序遍历:递归 —————————— */
    public static void recursionPostorderTraversal(TreeNode root) {
        if (root != null) {
            recursionPostorderTraversal(root.left);
            recursionPostorderTraversal(root.right);
            System.out.print("输出节点:" + root.val);
        }
    }

 


非递归实现

 

 

 


虽然也是使用栈结构做节点暂存,但是后序遍历和先序、中序遍历不太一样,要特别唠叨几句。

因为后序遍历在决定是否可以输出当前节点的值的时候,需要考虑其左右子树是否都已经遍历完成。显然一个游标已经不够用了,所以需要再设置一个游标 - lastNode,用来保存当前输出的节点,用来做为下次对比的依据。

 

过程简单分析:

若 node 的右节点 node.right 为 null,说明已经是最底层节点,直接输出。如:输出节点【8】的场景,node.right == null;
节点输出以后,把 lastNode 节点设置成当前节点,将当前游标节点node设置为空,下一轮就可以访问栈顶元素。如:输出节点【8】以后,lastNode 设置成8,node == null,程序进入下一次while循环,直接走到“node = treeNodeStack.peek();”上,查看栈顶元素,此时 node 被赋值 【6】;
若 lastNode 等于当前考查节点的右子树,表示该节点的左右子树都已经遍历完成,则可以输出当前节点。如:输出节点【6】的场景,node.right == lastNode == 8 。

    /** —————————— 后序遍历:非递归 —————————— */
    public static void postorderTraversal(TreeNode root) {
        if(root == null){
            return;
        }
        Stack<TreeNode> treeNodeStack = new Stack<TreeNode>();
        TreeNode node = root;
        TreeNode lastNode = root;
        while (node != null || !treeNodeStack.isEmpty()) {
            while (node != null) {
                treeNodeStack.push(node);
                node = node.left;
            }
            // 查看当前栈顶元素
            node = treeNodeStack.peek();
            // 如果其右子树也为空,或者右子树已经访问过,则可以直接输出当前节点的值
            if (node.right == null || node.right == lastNode) {
                System.out.print("输出节点:" + node.val);
                treeNodeStack.pop();
                // 把输出的节点赋值给lastNode游标,作为下次比对的依据
                lastNode = node;
                node = null;
            }
            // 否则,继续遍历右子树
            else {
                node = node.right;
            }
        }
    }

 

4.层序遍历
层序遍历二叉树,使用了队列结构-Queue(先进先出)来实现。

先将根节点入队列,只要队列不为空,然后出队列并访问,接着将访问节点的左右子树依次入队列。如此操作,可以保证上层的节点一定会在下层之前输出。

    /** —————————— 层序遍历 —————————— */
    public static void levelTraversal(TreeNode root){
        if(root == null)
            return;
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.add(root);
        while(!queue.isEmpty()){
            TreeNode temp =  queue.poll();
            System.out.println("输出节点:" + temp.val);
            if(temp.left != null)
                queue.add(temp.left);
            if(temp.right != null)
                queue.add(temp.right);
        }
    }

 

补充:层序遍历二叉树,并且分层打印?
原理和上面是一样的,为了记录每层该打印几条数据,我们需要再定义2个变量:star,end。

    public static ArrayList<ArrayList<Integer>> printTree(TreeNode pRoot) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        if (pRoot == null) {
            return res;
        }
        // start 记录从队列中取出的节点数量,end记录每行的节点数
        int start = 0;  // 还没开始从queue取数据,所以初始值是0
        int end = 1;    // 根节点只有1个,初始值是1
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(pRoot);
        List<Integer> tempList = new ArrayList<>();
        while(!queue.isEmpty()){
            TreeNode temp = queue.poll();
            tempList.add(temp.val);
            start ++;
            if (temp.left != null) {
                queue.add(temp.left);
            }
            if (temp.right != null) {
                queue.add(temp.right);
            }
            // 当从队列中取出的数量等于存入的父节点数量时,说明当前层已经遍历完,换行
            if (end == start) {
                res.add(new ArrayList<>(tempList));
                end = queue.size();
                start = 0;
                tempList.clear();
            }
        }
        return res;
    }

 

总结
二叉树的先序、中序、后序、层序遍历是面试算法题中的高频考点;
因为递归写法非常简单,建议一定要掌握非递归的方法,
非递归的方式非常利于解决场景问题,不管是面试出的场景,还是实际生产场景;
先序、中序、后序遍历使用栈结构(Stack)来解决节点暂存的问题;
先序、中序遍历过程类似,区别在节点的输出时机,而后序遍历因为需要考虑节点回溯后对比,所以引入了一个游标lastNode,用来保存上一次输出的Node;
层序遍历使用队列结构(Queue)来保障上下层节点的输出顺序;


原文链接:https://blog.csdn.net/weixin_44259720/article/details/121494049

标签:node,遍历,Java,递归,二叉树,null,root,节点
From: https://www.cnblogs.com/shoshana-kong/p/16977138.html

相关文章

  • 【数据结构-树】二叉树的相关算法
    目录1计算二叉树中双分支结点的个数2交换二叉树中所有左右子树3求先序遍历第k个元素4删去值为x的子树5计算二叉树的带权路径长度(WPL)6将表达式树转化为等价的中缀......
  • java学习笔记--java介绍,一些基本知识,面向对象的理解
    <1>Java介绍1)Java的特点简单易学    是c和c++的变种,而且摒弃了其中容易引起程序错误的地方,比如结构体,内存回收等。提供了丰富的类库。完全面向对象。安全性高......
  • JavaScript奇淫技巧:用密码保护你的照片
    JavaScript奇淫技巧:密码保护的私密图片JavaScript奇淫技巧:图片压缩、图片加密本文将用JavaScript实现两个颇有技术含量的功能:图片压缩、图片加密。最终效果:可实现将任意图片......
  • 小新学Java18-【函数式接口】
    一、函数式接口1.1概念1.2格式1.3@FunctionalInterface注解@FunctionalInterface注解作用:可以检测接口是否是一个函数式接口是:编译成功否:编......
  • 洛谷 P1113 杂务(拓扑排序,递归)
    题目大意:有一个有向无圈图,每个节点看作一个任务,一个任务需要完成必须先完成父亲节点的任务,每个任务都有耗时。假设现在所有不相关任务都可以并行执行,问最短多少时间可以把所......
  • 剑指offer 序列化二叉树
    题目描述请实现两个函数,分别用来序列化和反序列化二叉树 二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可......
  • 剑指offer 对称的二叉树(思维)
    题目描述请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。解题思路:正规的解法是传入两个节点,然后判断它们是不......
  • java环境搭建安装
    2.java环境搭建安装2.1JDK下载​开发人员要进行开发需要安装jdk,程序运行的话环境只需要jre,所以我们作为开发人员,需要下载jdk,最新版本为jdk17,下载地址为oracle官......
  • Java大型建筑建设综合工程项目管理系统源码带原生APP源码 工程管理系统源码带操作手册
    客户端下载:1、安卓手机端先下载“PP助手”应用商店,再在PP助手中搜索“star工程”下载安装。2、苹果手机端苹果手机APPstore中搜索“star工程”下载安装。建设综合工程......
  • 【校招VIP】线上实习 推电影 电影详情模块 Java文档周最佳
    【推电影】主要是为校招设计的年青人电影推荐平台项目,每个模块都具有亮点和难点,项目表现为手机网站应用,可嵌入小程序或APP中。恭喜来自南京邮电大学的日月同辉同学获得本......