java实现二叉树的前序、中序、后序以及层序遍历

java实现二叉树的前序、中序、后序以及层序遍历

前言:本文主要探究二叉树遍历的算法,关于二叉树的基础知识可以去看我的这篇文章:《JAVA实现平衡二叉树(AVL)》

数据构建

我们先来构建一颗二叉树的基本模型

在这里插入图片描述

先序遍历

规则:先根再左后右(所有子系也遵循该规则)

顺序:ABDGEHCF

作用:在第一次遍历到节点时就执行操作,一般只是想遍历执行操作(或输出结果)可选用先序遍历。

流程图

在这里插入图片描述

递归实现

/**
 * 前序遍历-递归
 * 顺序:根-左-后
 *
 * @param node
 */
public void preOrder(TreeNode node) {
    if (node == null) return;
    //打印根节点
    printNodeValue(node);
    //如果有左节点则打印左节点
    if (node.left_child != null) preOrder(node.left_child);
      //如果有右节点则打印右节点
    if (node.right_child != null) preOrder(node.right_child);
}

递归实现还是比较简单的,先遍历根节点,如果有左节点则先把左节点的子系遍历完,再去遍历右节点。

循环实现

  	   /**
         * 前序遍历-非递归
         * 顺序:根-左-后
         */
        public void preOrderNonRecursion(TreeNode root) {
            TreeNode node = root;//记录根节点
            Stack<TreeNode> stack = new Stack<>();//构建一个栈
            //开始循环,如果当前节点为空或栈为空则结束循环
            while (node != null || !stack.empty()) {
            	//如果当前节点不为空
                if (node != null) {
                    printNodeValue(node);//遍历当前节点
                    stack.push(node);//将节点压入栈
                    node = node.left_child;//当前记录的节点该为旧节点的左节点,即下次循环遍历左节点
                //如果当前节点为空
                } else {
                    TreeNode pop = stack.pop();//移出栈顶的节点,并返回被移除的节点
                    node = pop.right_child;//获取被移除节点的右节点,并记录为当前需要遍历的节点
                }
            }
        }

循环实现相较于递归来说比较麻烦,遍历当前节点的左节点很好处理,但是当没有后续子系需要遍历的时候就比较麻烦了,该如何回退到上一节点呢?

在这里插入图片描述

​ 此时我们需要用到栈(Stack),它的特性是先进后出(FILO, First In Last Out),先入栈的元素会被压入栈底,入栈出栈都是操作栈顶的数据。

在这里插入图片描述

遍历时,我们需要把遍历过的节点压入栈;当节点没有后续子系可遍历时,需要对当前的节点弹出栈,来模拟二叉树回退到上一层级的效果。

在这里插入图片描述

当前,我们也可以用双端队列(Deque)来实现这个效果(单端队列只支持一端进另一端出))

       /**
         * 前序遍历-非递归
         * 顺序:根-左-后
         */
        public void preOrderNonRecursion(TreeNode root) {
            TreeNode node = root;
            /**
             * 双端队列实现
             * 两种思路 模拟栈后入先出
             * 1.从头部插入元素从头取出元素
             * 2.从尾部插入元素从尾部取出元素
             */
            Deque<TreeNode> queue = new ConcurrentLinkedDeque<>();
            while (node != null || !queue.isEmpty()) {
                if (node != null) {
                    printNodeValue(node);
                    queue.offerLast(node);//将节点加入队列的头部
                    node = node.left_child;
                } else {
                    TreeNode last = queue.pollLast();//移除队列头部的元素
                    node = last.right_child;
                }
            }
        }

中序遍历

规则:先左再根后右(所有子系也遵循该规则)

顺序:GDBEHACF

作用:对于二分搜索树,中序遍历的操作顺序(或输出结果顺序)是符合从小到大(或从大到小)顺序的,故要遍历输出排序好的结果需要使用中序遍历

流程图

在这里插入图片描述

递归实现

/**
 * 中序遍历-递归
 * 顺序:左-根-后
 *
 * @param node 当前根系节点
 */
public void middleOrder(TreeNode node) {
    if (node == null) return;
    if (node.left_child != null) middleOrder(node.left_child);//如果左节点不为空,则继续向左探查
    printNodeValue(node);//输出节点值
    if (node.right_child != null) middleOrder(node.right_child);//如果右节点不为空,则继续向右探查

}

递归依旧比较简介,这里不详细介绍。

循环实现

  	   /**
         * 中序遍历-非递归
         * 顺序:左-根-后
         *
         * @param root 根节点
         */
        public void middleOrderNonRecursion(TreeNode root) {
            TreeNode node = root;
            Stack<TreeNode> stack = new Stack<>();
            while (node != null || !stack.empty()) {
                //如果node不为空,把node压入栈,并且继续向左探查
                if (node != null) {
                    stack.push(node);
                    node = node.left_child;
                //如果node为空,弹出栈顶的node,并打印该node的值
                } else {
                    TreeNode pop = stack.pop();
                    printNodeValue(pop);
                    //如果弹出栈node的右节点不为空,则继续向右探查
                    if (pop.right_child != null) {
                        node = pop.right_child;
                    }
                }
            }
        }

相较于前序,中序遍历只有在探查到树中最左边的节点时才会输出第一个节点。

后序遍历

规则:先左再右后根(所有子系也遵循该规则)

顺序:GDHEBFCA

作用:后续遍历的特点是执行操作时,肯定已经遍历过该节点的左右子节点,故适用于要进行破坏性操作的情况,比如删除所有节点。

流程图

在这里插入图片描述

递归实现

 	   /**
         * 后序遍历-递归
         * 顺序:左-右-根
         *
         * @param node 当前根系节点
         */
        public void postOrder(TreeNode node) {
            if (node == null) return;
            if (node.left_child != null) postOrder(node.left_child);//如果左节点不为空,则继续向左探查
            if (node.right_child != null) postOrder(node.right_child);//如果右节点不为空,则继续向右探查
            printNodeValue(node);//输出节点值
        }

循环实现

  	   /**
         * 后序遍历-非递归
         * 顺序:左-右-根
         *
         * @param root 根节点
         */
        public void postOrderNonRecursion(TreeNode root) {
            TreeNode node = root;//记录当前检索的节点
            TreeNode prev = null;//记录上一次访问过的节点
            Stack<TreeNode> stack = new Stack<>();
            while (node != null || !stack.isEmpty()) {
                //如果node不为空,把node压入栈,并且继续向左探查
                if (node != null) {
                    stack.push(node);
                    node = node.left_child;
                } else {
                    node = stack.peek();//根系节点记录为堆栈顶部的节点,注意peek()和pop()的区别,peek只获取元素,不弹出栈
                    if (node.right_child != null && node.right_child != prev) {//如果该节点的右节点不为空,并且没有被访问过
                        node = node.right_child;//把node记录为node的右子节点,继续向下探寻
                    } else {//如果右节点为空,或者不为空但是已经访问过
                        stack.pop();//把该节点弹出
                        printNodeValue(node);//打印该节点值
                        prev = node;//把上一次访问过的节点记录为当前检索的节点
                        node = null;//把当前检索的节点置空(防止重复打印该节点)
                    }
                }
            }
        }

这里多用了一个变量prev来记录上一次访问过的节点,如果当前节点是第一次访问并且没有后续的子系,则打印出该节点值,并记录该节点。

循环实现二(双栈)

	   /**
         * 后序遍历-非递归-双栈
         * 顺序:左-右-根
         *
         * @param root 根节点
         */
        public void postOrderNonRecursion2(TreeNode root) {
            TreeNode node = root;//记录当前检索的节点
            Stack<TreeNode> stack = new Stack<>();//临时检索栈
            Stack<TreeNode> result = new Stack<>();//结果栈
            //开始循环,如果当前节点为空或栈为空则结束循环
            while (node != null || !stack.isEmpty()) {
                //如果当前检索的节点不为空
                if (node != null) {
                    stack.push(node);//将节点压入临时检索栈
                    result.push(node);//将节点压入结果栈
                    node = node.right_child;//当前记录的节点改为检索节点的右节点,即下次循环遍历右节点
                } else {// 如果当前节点为空
                    TreeNode pop = stack.pop();//移出栈顶的节点,并返回被移除的节点
                    node = pop.left_child;//获取被移除节点的左节点,并记录为当前需要遍历的节点
                }
            }
            //循环遍历结果栈,直到栈空
            while (!result.isEmpty()) {
                printNodeValue(result.pop());
            }
        }

这种方法实现思路比较取巧,首先前序遍历的顺序为:根-左-右,我们把前序遍历的顺序稍微改变一下:根-右-左,每次遍历出需要打印的节点值,不直接执行打印,而是把该节点存入第二个栈(结果栈)中;当所有节点都检索完毕时,对结果栈中的元素执行出栈并打印,则此时出栈的顺序为:左-右-根

在这里插入图片描述

层序遍历

规则:把二叉树分层,每一层从左到右遍历

顺序:ABCDEFGH

作用:前面所将的前序、中序、后序遍历的策略为DFS(深度优先搜索),而层序遍历策略为BFS (广度优先搜索)。如果我们使用 DFS/BFS 只是为了遍历一棵树、一张图上的所有结点的话,那么 DFS 和 BFS 的能力没什么差别,我们当然更倾向于更方便写、空间复杂度更低的 DFS 遍历。不过,某些使用场景是 DFS 做不到的,只能使用 BFS 遍历。这就是本文要介绍的两个场景:「层序遍历」、「最短路径」。

流程图

虽然 DFS 与 BFS 都是将二叉树的所有结点遍历了一遍,但它们遍历结点的顺序不同。

在这里插入图片描述

递归实现


       /**
         * 层序遍历-递归
         * 顺序:按照层次由左向右输出
         *
         * @param root 当前根系节点
         */
        public void levelOrder(TreeNode root) {
            if (root == null) return;
            int depth = countNodeDepth(root);//计算节点的深度
            for (int i = 1; i <= depth; i++) {//从根节点的第一层级开始遍历
                levelOrderRecursive(root, i);
            }
        }

	  	 /**
         * 递归计算节点的深度
         * 如果该节点没有子节点,则深度为0
         * 如果有子节点,则深度为左子节点和右子节点的最大值+1
         *
         * @param cur_node 当前节点
         * @return 节点深度
         */
        private int countNodeDepth(TreeNode cur_node) {
            if (cur_node == null) {
                return 0;//当前节点为空则返深度为0
            }
            //深度为左树和右树中最大深度+1
            return 1 + Math.max(countNodeDepth(cur_node.left_child), countNodeDepth(cur_node.right_child));
        }

       /**
         * 递归遍历
         *
         * @param node 当前节点
         * @param level 当前层级
         */
        private void levelOrderRecursive(TreeNode node, int level) {
            if (node == null || level < 1) return;//当前节点为空或者层级小于1退出递归
            if (level == 1) printNodeValue(node);//层级等于1打印当前节点值
            //打印下一层级的节点
            levelOrderRecursive(node.left_child, level - 1);
            levelOrderRecursive(node.right_child, level - 1);
        }

节点深度计算

在这里插入图片描述

以上图为例,计算根节点A的深度;由图知根节点左树节点有:B、D、E、G、H,右树中节点有:C、F;其中,左树中层级最深的节点为:G、H,层级为3,右树中层级最深的节点为:F,层级为2;由于此树左树比右树深,采用左树中最的深的节点来计算,则根节点A的深度为:节点G(或H)的深度+1(1代表自己本身的层级)。

递归详解

这个算法的思想是:每次for循环都从根部重新去遍历一遍节点,每遍历完一次下一次遍历都往下增加一层,只有当前遍历的层数属于未遍历过的层数的时候,才去打印节点值。

在这里插入图片描述

计算结果
A
B
C
D
E
F
G
H

根据上面的算法可以得出一个线性的结果,现在我们增加点难度,让结果变为层级+层级节点的形式。

递归实现二

	     /**
         * 层序遍历-递归
         * 顺序:按照层次由左向右输出并返回二维数组
         *
         * @param root 当前根系节点
         */
        public void levelOrderToArray(TreeNode root) {
            if (root == null) return;
            helper(root, 0);//递归计算层级
            printRes(res);//打印结果
        }

				//根据层级保存遍历的结果
        List<List<Integer>> res = new ArrayList<List<Integer>>();

        /**
         * 递归计算层级,并存入队列
         *
         * @param node 计算节点
         * @param level 节点层级
         */
        private void helper(TreeNode node, int level) {
             if (res.size() == level) res.add(new ArrayList<Integer>());//动态增加队列长度
            res.get(level).add(node.data);//把节点值根据层级储存在队列中
            //判断该节点有无左右子树,若有则递归向下计算
            if (node.left_child != null) helper(node.left_child, level + 1);
            if (node.right_child != null) helper(node.right_child, level + 1);
        }

  		 /**
         * 打印层级遍历的结果
         *
         * @param res
         */
        private void printRes(List<List<Integer>> res) {
            if (res == null || res.size() <= 0) return;
            for (int i = 0; i < res.size(); i++)
                System.out.println(i + ":" + res.get(i).toString());
        }
        
计算结果
0:[A]
1:[B, C]
2:[D, E, F]
3:[G, H]

算法二相较于一做了一些改动,首先用一个队列保存每次层级遍历的结果,其次递归停止调用的逻辑由进入停止变为停止进入,改进了递归的逻辑,不需要每一次都从根部去遍历;

在执行遍历的时候,会先遍历左子树,再去遍历右子树,并根据层级储存在队列中。

在这里插入图片描述

循环实现

 		   /**
         * 层序遍历-循环
         * 顺序:按照层次由左向右输出
         *
         * @param root 根节点
         */
        public void levelOrderNonRecursion(TreeNode root) {
            Queue<TreeNode> queue = new ArrayDeque<>();
           if (root != null) queue.add(root);//把根节点加入队列
            //如果队列不为空则循环继续
            while (!queue.isEmpty()) {
                TreeNode poll = queue.poll();//队列头节点出队并返回该元素
                printNodeValue(poll);//打印节点值
              	//如果该节点的左右子树不为空则加入队尾
                if (poll.left_child != null) queue.add(poll.left_child);
                if (poll.right_child != null) queue.add(poll.right_child);
            }
        }
计算结果
A
B
C
D
E
F
G
H

虽然和递归实现二同样是使用队列储存,但这里执行遍历的顺序为:根-左-右,三个一组,打印完一组再去递归遍历子系;我们尝试把遍历结果也变成层级+层级节点的形式。

在这里插入图片描述

循环实现二

       /**
         * 层序遍历-循环
         * 顺序:按照层次由左向右输出并返回二维数组
         *
         * @param root 根节点
         */
        public void levelOrderNonRecursionToArray(TreeNode root) {
            Queue<TreeNode> queue = new ArrayDeque<>();//临时储存节点的队列
            List<List<Integer>> res = new ArrayList<>();//记录遍历结果的集合
            if (root != null) queue.add(root);
        		//如果队列不为空则继续循环
            while (!queue.isEmpty()) {
                int n = queue.size();//记录队列的长度
                List<Integer> level = new ArrayList<>();//储存层级节点值的集合
                for (int i = 0; i < n; i++) {
                    TreeNode poll = queue.poll();//出队并返回出队的元素
                    level.add(poll.data);//把节点值加入集合
                    	//如果该节点的左右子树不为空则执行入队
                    if (poll.left_child != null) queue.add(poll.left_child);
                    if (poll.right_child != null) queue.add(poll.right_child);
                }
                res.add(level);//把层级结果加入结果集
            }
            printRes(res);//结束循环打印结果集
        }
计算结果
0:[A]
1:[B, C]
2:[D, E, F]
3:[G, H]

原理和层序遍历递归实现二有些相似,和循环一的不同在于while循环里面嵌套了一个for循环,并实时记录下了当前队列的长度,只有在for循环中把当前层级的节点全部出列才会进入下一次while循环。

Z型层序遍历

规则:把二叉树分层,奇数层从左往右遍历,偶数层从右往左遍历

顺序:ACBDEFHG

作用:同上

流程图

在这里插入图片描述

递归实现

        /**
         * z-型层序遍历-递归
         * 顺序:按照层次,奇数层由右向左输出,偶数层由左向右
         *
         * @param root 当前根系节点
         */
        public void z_levelOrderToArray(TreeNode root) {
            if (root == null) return;
            helper(root, 0);//递归计算层级
            //遍历结果集,根据层级判断是否要反转集合
            for (int i = 0; i < res.size(); i++) {
                //如果是奇数层则反转
                if (i % 2 != 0) {
                    Collections.reverse(res.get(i));
                }
            }
            printRes(res);//打印结果
        }

        List<List<Integer>> res = new ArrayList<List<Integer>>();

和层序递归实现二的思路差不多,在得到结果集后,根据当前层级来判断是否要将层级元素反转。(这里只贴有改动的部分)

循环实现

			 /**
         * z-型层序遍历-循环
         * 顺序:按照层次,奇数层由左向右输出,偶数层由右向左输出
         *
         * @param root 根节点
         */
        public void z_levelOrderToArray(TreeNode root) {
            //此处queue的作用主要是做临时的节点入队和出队,故用LinkedList效率比较高
            Queue<TreeNode> queue = new LinkedList<>();//储存驱动while循环需要的节点(while循环完一轮长度会动态变化)
            List<List<TreeNode>> res = new ArrayList<>();//结果集(储存的为节点实体)
            //level的作用是查询当前层级节点的所有子系,故用ArrayList效率比较高
            List<TreeNode> level = new ArrayList<>();//结果集的层级对象(即二位数组每一层储存的内容)
            if (root != null) queue.add(root);//根节点入队queue
            //如果队列不为空则继续循环
            while (!queue.isEmpty()) {
                int n = queue.size();//重新计算deque的长度
                level.clear();//level清空(此处level的数据是通过IO流深拷贝存入res中,所以清空不会对res的内容造成影响)

                //第一个for循环用于将queue中的节点全部出列,并存入层级集合level(此时的queue包含当前层级的所有节点)
                for (int i = 0; i < n; i++) {
                    TreeNode poll = queue.poll();//出队并返回出队的元素
                    level.add(poll);//把出队的节点加入层级集合level
                }
                res.add(depCopy(level));//把层级集合level的数据拷贝一份存入结果集

                //第二个for循环用于将下一层级的节点按照规律存入queue,来驱动下一次while循环
                //此处的规律为:奇数层由左向右输出,偶数层由右向左输出
                for (int j = 0; j < level.size(); j++) {//将level中所有节点的子节点遍历取出,加入queue(即遍历取出下一层的所有节点)
                    //从level的尾节点往前遍历
                    TreeNode node = level.get(level.size() - 1 - j);

                    //根据res的长度来判断下一层级应该从左往右遍历还是从右往左遍历(res的长度同时也表示level加入时的层数)
                    //比如当前res的长度为1(奇数层--从左往右),那么下一层必定是偶数层,入队的顺序需要反转(从右往左)
                    //如果当前res的长度为2(偶数层--从从右往左),那么下一层必定是奇数层,入队的顺序需要反转(从左往右)
                    if (res.size() % 2 == 0) {
                        if (node.left_child != null) queue.add(node.left_child);
                        if (node.right_child != null) queue.add(node.right_child);
                    } else {
                        if (node.right_child != null) queue.add(node.right_child);
                        if (node.left_child != null) queue.add(node.left_child);
                    }
                }
            }
            printResTheNode(res);//打印结果集
        }

        /**
         * 对源集合进行深拷贝(只复制源集合数据不复制引用)
         *
         * @param srcList 源集合
         * @param <T>
         * @return
         */
        private <T> List<T> depCopy(List<T> srcList) {
            ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
            try {
                ObjectOutputStream out = new ObjectOutputStream(byteOut);
                out.writeObject(srcList);

                ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
                ObjectInputStream inStream = new ObjectInputStream(byteIn);
                List<T> destList = (List<T>) inStream.readObject();
                return destList;
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            return null;
        }

循环实现的思路和递归有所不同,递归是在结果集中判断节点所属层级的奇偶,来决定是否反序输出;而循环是先判断下一层级的奇偶,来决定节点的访问的顺序

在这里插入图片描述

循环改进(Z型镜像遍历)

我们修改一下上面的方法,让它支持镜像Z型遍历,即镜像模式下奇数层:右-左,偶数层:左-右

在这里插入图片描述

			  /**
         * z-型层序遍历(循环实现)
         * 顺序:按照层次,奇数层由左向右输出,偶数层由右向左输出
         *
         * @param root        根节点
         * @param isMirroring 是否启用镜像Z
         */
        public void z_levelOrderToArray2(TreeNode root, boolean isMirroring) {
            //此处queue的作用主要是做临时的节点入队和出队,故用LinkedList效率比较高
            Queue<TreeNode> queue = new LinkedList<>();//储存驱动while循环需要的节点(while循环完一轮长度会动态变化)
            List<List<TreeNode>> res = new ArrayList<>();//结果集(储存的为节点实体)
            //level的作用是查询当前层级节点的所有子系,故用ArrayList效率比较高
            List<TreeNode> level = new ArrayList<>();//结果集的层级对象(即二位数组每一层储存的内容)
            if (root != null) queue.add(root);//根节点入队queue
            //如果队列不为空则继续循环
            while (!queue.isEmpty()) {
                int n = queue.size();//重新计算queue的长度
                level.clear();//level清空(此处level的数据是通过IO流深拷贝存入res中,所以清空不会对res的内容造成影响)

                //第一个for循环用于将queue中的节点全部出列,并存入层级集合level(此时的queue包含当前层级的所有节点)
                for (int i = 0; i < n; i++) {
                    TreeNode poll = queue.poll();//出队并返回出队的元素
                    level.add(poll);//把出队的节点加入层级集合level
                }
                    res.add(depCopy(level));//把层级集合level的数据拷贝一份存入结果集

                //第二个for循环用于将下一层级的节点按照规律存入queue,来驱动下一次while循环
                for (int j = 0; j < level.size(); j++) {//将level中所有节点的子节点遍历取出,加入queue(即遍历取出下一层的所有节点)
                    //从level的尾节点往前遍历
                    TreeNode node = level.get(level.size() - 1 - j);
                    /**
                     * 根据res的长度来判断下一层级应该从左往右遍历还是从右往左遍历(res的长度同时也表示level加入时的层数)
                     * 比如当前res的长度为1(奇数层--从左往右),那么下一层必定是偶数层,入队的顺序需要反转(从右往左)
                     * 如果当前res的长度为2(偶数层--从从右往左),那么下一层必定是奇数层,入队的顺序需要反转(从左往右)
                     */
                    int remainder = res.size() % 2;//计算余数,根据余数是否为0判断当前层数是奇数层还是偶数层
                    //判断是否开启镜像
                    if (!isMirroring) {
                        //未开启镜像模式下,奇数层从左往右
                        if (remainder == 0) {
                            leftToRight(node, queue);
                        } else {
                            rightToLeft(node, queue);
                        }
                    } else {
                        //开启镜像模式下,奇数层从右往左
                        if (remainder == 0) {
                            rightToLeft(node, queue);
                        } else {
                            leftToRight(node, queue);
                        }
                    }
                }
            }
            printResTheNode(res);//打印结果集
        }
        

        /**
         * 先左后右
         *
         * @param node  需要遍历的节点
         * @param queue 存入子节点的队列
         */
        private void leftToRight(TreeNode node, Queue<TreeNode> queue) {
            if (node.left_child != null) queue.add(node.left_child);
            if (node.right_child != null) queue.add(node.right_child);
        }

        /**
         * 先右后左
         *
         * @param node  需要遍历的节点
         * @param queue 存入子节点的队列
         */
        private void rightToLeft(TreeNode node, Queue<TreeNode> queue) {
            if (node.right_child != null) queue.add(node.right_child);
            if (node.left_child != null) queue.add(node.left_child);
        }

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值