二叉树的遍历(完结)

二叉树的三种常用遍历方式

     学习过数据结构的同学都清楚,除了层序遍历外,二叉树主要有三种遍历方式:
         1. 先序遍历
         2. 中序遍历
         3. 后序遍历
     关于这三种遍历方式,我不做赘述,** 网上或者书本上,大家可以去了解一下。 ** 这次我们主要总结一下二叉树遍历的算法的递归实现和非递归实现。而重点又落在非递归实现上,因为,递归实现,其实也是书本上的基础知识,只要去查阅机会有所理解。

递归实现

先序遍历的递归实现

     我们先定义一个二叉树的节点结构作为本博文中的通用节点。

public class Node{
    public int value;
    public Node left;
    public Node right;
    
    public Node(int data){
        this.value = data;
    }
}

     我们知道,二叉树的递归遍历无非是对一颗非空的二叉树的每一个节点,做相同的操作(先序、中序、后序)。二叉树的先序遍历就是对每一个节点来先序遍历的动作。代码如下:

public void preOrder(Node head){
    if(head == null)return;
    
    System.out.print(head.value + " ");
    preOrder(head.left);
    preOrder(head.right);
}

中序遍历的递归实现

     同样的,二叉树中序遍历的递归实现,也是建立在同样的思想上。

public void midOrder(Node head){
    if(head == null)return;
    
    midOrder(head.left);
    System.out.print(head.value);
    midOrder(head.right);
}

后序遍历的递归实现

     ** The same idea , not the same recipe. **

public void posOrder(Node head){
    if(head == null)return;
    
    posOrder(head.left);
    posOrder(head.right);
    System.out.print(head.value);
}

关于递归实现的简单总结

     从上面的代码,我们发现,在递归实现上,三种遍历方式的主要区别就是输出当前头结点的值的时机,从而让遍历的顺序产生区别。

非递归实现

用递归方法解决的问题都可以用非递归来实现,因为递归方法无非是实用函数栈来保存信息,如果用自己定义的栈来代替函数栈,就可以实现相同的功能。

先序遍历的非递归实现

     先来大致分析一下非递归方式实现先序遍历的具体过程:
         1. 申请一个新的stack,然后把头结点打入stack中
         2. 从stack中弹出栈顶节点,记为 head ,然后打印 head 的值,然后 ** 把 head 的右孩子压入栈中,再把 head 的左孩子压入栈中(注意先后顺序)。 ** 当然在做这些操作之前,都需要一个 ** 非空判断. ** 。
         3. 重复步骤2,直到 stack 为空。

     到这里,我们贴上一段代码:

public void preOrderNoRecur(Node head){
    if(head != null){
    //新建一个栈空间,存储节点,对应过程中的第一步
        Stack<Node> stack = new Stack<Node>();
        //把头结点压入栈中
        stack.add(head);
        while(! stack.isEmpty()){
            head = stack.pop();
            System.out.print(head.value + " ");
            //分别对弹出节点的右孩子、左孩子判空,按照顺序压入栈中
            if(head.right != null)stack.add(head.right);
            if(head.left != null)stack.add(head.left)
        }
    }
}

** 需要重点理解的就是弹出节点的左右孩子被压入栈的时机。 **建议大家自行画一个图,模拟一下程序运行的过程。

中序遍历的非递归实现

     同样的,我们先分析一下中序遍历非递归实现的具体过程:
         1. 申请一个新的 stack 。** 跟先序遍历不同的是 ** ,我们需要两个值,一个cur,记录当前遍历到的节点的值,一个head,记录当前二叉树的根节点。** 初始时,cur = head ** 。
         2. 把cur代表的节点压入栈中,让cur = cur.left
         3. 重复步骤2,直到 cur 为空时,从 stack 中弹出一个节点,记为 outNode ,打印 outNode 的值。然后让 cur = outNode.right,继续重复步骤2。
         4. 当 stack 为空且 cur 为空时,整个遍历完成。闲话不多说,上代码。

public void midOrderNoRecur(Node head){
    if(head != null){
        Stack<Node> stack = new Stack<Node>();
        Node cur = head;
        while(!stack.isEmpty() || cur != null){
            if(cur != null){
            //步骤二,不停的把以 cur 的左孩子压入栈中
                stack.push(head);
                cur = cur.left;
            }else{
            //当 cur 为空时,我们弹出栈顶元素,然后对其右孩子做相同的事
                Node outNode = stack.pop();
                System.out.print(outNode.value + " ");
                cur = outNode.right;
            }
        }
    }
}

     ** 实际上, ** 我们可以把这段程序中的 cur , outNode都替换成head,这样做可以节约空间,但是在代码清晰度上个人觉得会有所降低。因为是用来总结,所以觉得还是把各个东西写清晰的好。但是也附上都替换成 head 后的代码。

public void midOrderNoRecur(Node head){
    if(head != null){
        Stack<Node> stack = new Stack<Node>();
        while(!stack.isEmpty() || head != null){
            if(head != null){
            //步骤二,不停的把以 head 的左孩子压入栈中
                stack.push(head);
                head = head.left;
            }else{
            //当 head 为空时,我们弹出栈顶元素,然后对其右孩子做相同的事
                head = stack.pop();
                System.out.print(head.value + " ");
                head = head.right;
            }
        }
    }
}

     此处预留一个位置,今天实在太晚了,困的不行,接下面的明天继续更。Good Night.
     更新……

后序遍历的非递归实现

     关于二叉树后序遍历的非递归实现,我学习了两种方式。一种是利用两个栈,另一种是利用一个栈。

两个栈实现后序遍历

     依然从具体步骤开始:
         1. 申请两个 stack,分别记为 s1,s2 ,然后将头结点压入 s1 中
         2. 从 s1 中弹出一个节点,记为 cur ,然后 cur 的左孩子和右孩子按顺序压入栈中
         3. 把 cur 压入 s2 中
         4. 不断重复第二步、第三步、一直到 s1 为空时停止
         5. 从 s2 中依次弹出节点并打印值

public void posOrderNoRecur(Node head){
    if(head != null){
    //申请两个栈空间
        Stack<Node> s1 = new Stack<Node>();
        Stack<Node> s2 = new Stack<Node>();
        //把头结点压入栈s1中
        s1.push(head);
        while(!s1.isEmpty()){
        //从s1中弹出一个节点,并且分别对其左右孩子判空,压入s1
            Node cur = head.pop();
            //把弹出的节点压入s2
            s2.push(cur);
            if(cur.left != null) s1.push(cur.left);
            if(cur.right != null) s2.push(cur.right);
        }
        
        while(!s2.isEmpty())
            System.out.print(s2.pop().value + " ");
    }
}

** 以上,我们便利用两个栈完成了二叉树后序遍历的非递归实现,思想上很简单,就是对每个节点做一次先序遍历,然后再按照右孩子,左孩子的顺序压入 s2 中,因为我们知道栈的特性之一就是先进后出,按照前面所说的顺序把节点压入栈中,再遍历时就会得到一个正确的后序遍历。同样建议大家自己在稿纸上画图,模拟过程,帮助理解。 **

一个栈实现后序遍历

** 明天继续… **

     用一个栈来实现二叉树的后序遍历,多少会有一些麻烦,我决定采用先贴出代码的方式。

public void posOrderNoRecur(Node h){
    if(h != null){
        Stack<Node> stack = new Stack<Node>();
        stack.push(h);
        Node c = null;
        while(!stack.isEmpty()){
            c = stack.peek();
            if(c.left != null && h != c.left && h != c.right){
                stack.push(c.left);
            }else if(c.right != null && h != c.right){
                stack.push(c.right);
            }else{
                System.out.print(stack.pop().value + " ");
                h = c;
            }
        }
    }
}

     然后再来说代码的实现思路,主要是遵从这样的步骤:
         1. 首先,依然是申请一个栈,记为 stack , 并且设置两个变量 h 和 c。在整个过程中,h 代表最近一次弹出并打印的节点,c 代表栈顶节点,初始情况: h 为 head , c 为 null。
         2. 这个时候(h 为最新弹出节点,c 为栈顶节点< ** 并没有弹出 ** >)有三种情况:
             * 如果 c 的左孩子不为空,且 h 不是 c 的左孩子也不是 c 的右孩子,那么就把 c 的左孩子压入栈中。
             * 如果 c 的又孩子不为空,且 h 不是 c 的右孩子,那我们就把 c 的右孩子压入栈中。
             * 如果上述两种情况都不满足,说明 c 的左子树和右子树都已经打印完毕,然后让 h = c.
             * 结束条件: 栈 stack 为空

     接下来我们解释上述步骤的逻辑,首先,我们刚开始的时候把 h 代表的头结点压入 stack 中,此时,栈顶节点即为树的根,我们另 c = stack.peek(),这个时候,按照我们对后序遍历的理解,我们应该先遍历头结点的左子树,如果 c 的 ** 左孩子不为空 ** 的话,这个时候我们的 h 肯定不等于 c 的左孩子/右孩子,因为我们并没有把左孩子/右孩子压入栈中,所以我们把 c 的左孩子压入栈中,而之所以判断一下 h 所代表的刚弹出的节点是否为 c 的左孩子/右孩子,是因为我们要判断 c 的左右孩子是否有遍历。如果有遍历,那我们自然可以判断, c 的左右孩子已经遍历过,弹出 c 节点打印值,然后进入下一次循环即可。同理的,当 c 的左孩子为空,右孩子不为空时,我们自然要判断刚弹出的是否是 c 的右孩子,然后判断把 c 的右孩子压入栈中。
     我们讲了很多关于压入栈的操作,但是我们什么时候输出呢,也就是什么时候得到 h 的值呢。最后一个 else 包含的情况有哪些呢。很明显,就是遍历到叶子节点的时候。当我们碰到栈顶节点是叶子节点的时候,就会产生弹出。

总结

     本文主要讲了二叉树非常基础的三种遍历方式: 先序遍历、中序遍历、后序遍历。在实现方式上,我们主要区分递归的实现方式和非递归的实现方式。递归的方式是书本上的内容,非递归方式的话,其实都很好理解,主要应该注意的就是压入栈的时机,左右孩子压入栈的顺序。这些顺序的判断和分析,都是建立在先/中/后序遍历的特点上的。只要清楚所应该遵循的规则,相信就可以写出正确的程序。

     Ps: 花了四天才写完这篇博客。第一天太晚,第二天感冒,第三天出去浪了。是时候提高一波执行力了。
     ** 博客地址 :** 博客

    原文作者:SherlockBlaze
    原文地址: https://www.jianshu.com/p/56296e206edd
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞