Java实现二叉树的先序、中序、后序遍历算法

Java实现二叉树的先序、中序、后序遍历算法

一、构建二叉树的存储结构

一棵二叉树的节点结构主要包括三部分:节点的值域,指向左孩子的指针,指向右孩子的指针。由于Java语言没有指针的定义,这里采用属性的方式实现(重写equals方法是为了后续遍历非递归算法的需要,后面会说明)。代码如下:

import java.util.Objects;

/**
 * @author:Chris
 * @description 二叉树结点结构
 */
public class BiTNode {

    char val;       //节点的值域
    BiTNode lChild; //指向左孩子
    BiTNode rChild; //指向右孩子
    
    public BiTNode(char val) {
        this.val = val;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BiTNode biTNode = (BiTNode) o;
        return val == biTNode.val &&
                Objects.equals(lChild, biTNode.lChild) &&
                Objects.equals(rChild, biTNode.rChild);
    }

}

二、先序、中序与后续遍历的递归算法

2.1 先序遍历

简单来说,就是从一棵树的根节点开始,先访问根节点,然后依次访问左孩子和右孩子节点。也就是“根左右”的次序。
举个例子,如图2.1所表示的二叉树。从根节点出发依次遍历,A节点为根节点,访问A节点;然后遍历左孩子B节点,由于B节点也是所在子树的根节点,所以访问B节点;再继续遍历B节点的左孩子,由于为空,不做处理;继续遍历B节点的右孩子,访问D节点;D节点的左右孩子均为空,不做处理。这样,以A节点为根的左子树就遍历结束了。再依次遍历A节点的右子树,最后得到的先序遍历序列为:A - B - D - C - E
代码如下:

	/**
     * 先序遍历(递归)
     * @param bt
     */
    public void PreOrder(BiTNode bt) {
        if (bt != null) {
            System.out.print(bt.val + " ");
            PreOrder(bt.lChild);
            PreOrder(bt.rChild);
        }
    }

二叉树

图2.1 二叉树示例

2.2 中序遍历

中序遍历的访问次序为:左、根、右
如图2.1所示,从根节点出发,节点A左孩子非空,则应向左遍历;遇到节点B,遍历节点B的左孩子,发现左孩子为空;此时再次回到节点B,访问节点B;由于节点B的右孩子非空,继续向右遍历;节点D的左右孩子均为空,访问节点D;此时以节点A为根的左子树均被访问过了,然后访问节点A,然后遍历A的右子树。最终的访问次序为:B - D - A - C - E。需要注意的是,这里“遍历”这个词,更准确的描述应该是“经过”或“路过”。与先序遍历相比,先序遍历第一次路过根节点,根节点即被访问;而中序遍历时,根节点被第二次路过时,才会被访问。

代码如下:

	/**
     * 中序遍历(递归)
     * @param bt
     */
    public void InOrder(BiTNode bt) {
        if (bt != null) {
            InOrder(bt.lChild);
            System.out.print(bt.val + " ");
            InOrder(bt.rChild);
        }
    }

2.3 后续遍历

后续遍历的访问次序为:左、右、根
还是以图2.1为例,从根节点出发,优先向左遍历。首次经过节点B,继续向左 (注意:虽然节点B的左子树为空,但是也需要进行一次判断,即经过一次)。节点B左子树为空,继续遍历节点B的右子树 (此时节点B被第二次经过)。对于节点D,左右子树均为空,访问节点D。再次回到节点B,访问节点B (此时节点B被第三次经过时,才被访问)。此时以节点A为根的左子树就均被访问过了。继续遍历节点A的右子树,最后访问节点A,遍历结束。最终访问的次序为:D - B - E - C - A
代码如下:

	/**
     * 后序遍历(递归)
     *
     * @param bt
     */
    public void PostOrder(BiTNode bt) {
        if (bt != null) {
            PostOrder(bt.lChild);
            PostOrder(bt.rChild);
            System.out.print(bt.val + " ");
        }
    }

三、初始化一棵二叉树

这里将采用先序遍历的方式构造一棵二叉树,如图2.1所示。初始化输入各节点时,将空节点记为:’#’。

    /**
     * 先序遍历构造二叉树
     * @param bt
     * @return BiTNode
     */
    public BiTNode Create(BiTNode bt) {
        Scanner scanner = new Scanner(System.in);
        String str = scanner.next();
        char s = str.charAt(0);
        if (s == '#') {
            bt = null;
        } else {
            bt = new BiTNode(s);
            bt.lChild = Create(bt.lChild);
            bt.rChild = Create(bt.rChild);
        }
        return bt;
    }

四、测试

新建一个测试类名为 TreeTest,将上述遍历操作放在名为 TreeExer的类下面。在 TreeTest的main方法中进行测试。代码如下:

public class TreeTest {

    public static void main(String[] args) {
        TreeExer treeExer = new TreeExer();
        BiTNode root = new BiTNode();
        root = treeExer.Create(root);
        //先序遍历递归算法
        treeExer.PreOrder(root);
        //中序遍历递归算法
//        treeExer.InOrder(root);
        //后续遍历递归算法
//        treeExer.PostOrder(root);
    }

}

以图2.1为例,数据录入,输出先序遍历递归算法的结果如图4.1。(其他算法的调用读者可以自行尝试)
先序遍历递归算法测试

图4.1 先序遍历递归算法测试

五、先序、中序、后序遍历的非递归算法

将递归算法改写为非递归算法,往往需要栈这种数据结构。其实递归算法底层的实现也是通过栈来记录每次调用函数的位置。
idea中可以在System.out.print(bt.val + " ");处打断点进行调试,可以清楚的看到递归工作栈中递归调用的信息,如图5.1。

5.1 先序遍历(非递归)

先序遍历的非递归算法也是借助栈来实现。由于是先访问根节点,所以可以选择在入栈之前,对根节点进行访问。

代码如下:

	/**
     * 先序遍历的非递归算法
     *
     * @param bt
     */
    public static void PreOrder(BiTNode bt) {
        Stack<BiTNode> st = new Stack<>();      //用于保存节点

        BiTNode p = bt;                         //工作指针

        while (p != null || !st.isEmpty()) {
            if (p != null) {
                visit(p);
                st.push(p);                     //这里也可以使用add(Object obj)方法,线程安全的。因为Stack类继承自Vector类
                p = p.lChild;
            } else {
                p = st.pop();                   //如果节点为空,栈顶元素出栈
                p = p.rChild;                   //指向右子树
            }
        }
    }

函数调用栈

图5.1 断点调试

5.2 中序遍历(非递归)

与先序遍历的非递归算法唯一不同的是,visit函数的位置不同。这里是出栈后,对目标节点进行访问。也对应了前面提到的"第二次经过"才对其访问。
代码如下:

	/**
     * 中序遍历的非递归算法
     *
     * @param bt
     */
    public static void InOrder(BiTNode bt) {
        Stack<BiTNode> st = new Stack<>();      //用于保存节点

        BiTNode p = bt;                         //工作指针

        while (p != null || !st.isEmpty()) {
            if (p != null) {
                st.push(p);                     //这里也可以使用add(Object obj)方法,线程安全的。因为Stack类继承自Vector类
                p = p.lChild;
            } else {
                p = st.pop();                   //如果节点为空,栈顶元素出栈
                visit(p);
                p = p.rChild;                   //指向右子树
            }
        }
    }
    
	/**
     * 访问该节点
     * @param bt
     */
    private static void visit(BiTNode bt) {
        System.out.print(bt.val + " ");
    }

5.3 后续遍历(非递归)

在后续遍历中,要保证左孩子和右孩子都已经被访问,并且左孩子要在右孩子前访问,最后再访问根节点,这就为流程控制带来了困难。
问题的关键在于,当经过根节点时如何判断是从左子树返回的还是从右子树返回的。因此,声明一个指针r,它用于记录最近访问过的节点。也可以在类BiTNode中增加一个标志属性,记录是否已经被访问过。这里采用第一种方式实现。

代码如下:

	/**
     * 后续遍历的非递归算法
     *
     * @param bt
     */
    public static void PostOrder(BiTNode bt) {

        BiTNode p = bt;             //工作节点,用于访问每个节点

        BiTNode r = null;           //记录前一个被访问的节点

        Stack<BiTNode> st = new Stack<>();

        while (p != null || !st.isEmpty()) {
            if (p != null) {
                st.push(p);         //节点非空,则入栈
                p = p.lChild;       //指向左子树
            } else {
                p = st.peek();      //读栈顶元素
                if (p.rChild != null && p.rChild.equals(r) == false) {       //右孩子节点非空,且未被访问
                    p = p.rChild;   //转向右子树
                    st.push(p);	
                    p = p.lChild;   //再次指向左孩子
                } else {            //右子树为空,或已经访问过
                    p = st.pop();   //栈顶元素出栈
                    visit(p);
                    r = p;          //记录最近访问过的节点
                    p = null;       //注意:p需要置空
                }
            }
        }
    }

六、总结

先序遍历、中序遍历和后续遍历是二叉树相关问题的核心操作,也是很多题目的核心思想。其中后序遍历非递归算法应用场景有很多,例如:求根节点到某节点的路径、求两个节点的最近公共祖先等等。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值