二叉树的遍历(递归,迭代,Mirrors)详解

二叉树的遍历算法分为三种

1. 递归
2. 迭代
3. Mirrors遍历

本文主要讲述Mirror遍历与迭代算法,递归算法很简单,不再专门重述。
本文所使用的创建树的代码如下,思想采用的是层序遍历的逆向:

创建二叉树
public static TreeNode createTree(Integer[] root){
        if (root == null || root.length == 0)
            return null;
        TreeNode[] nodes = new TreeNode[root.length];
        nodes[0] = new TreeNode(root[0]);
        int floor = 1;
        int levels = (int) Math.ceil(Math.log(root.length+1)) + 1;
        while (floor < levels){
            int start = (int) (Math.pow(2,floor)-1);
            int end = (int) (Math.pow(2,floor+1)-1);
            if (end > root.length)
                end = root.length;
            for (int i = start; i < end; i++) {
                if (root[i] != null) {
                    nodes[i] = new TreeNode(root[i]);
                }
                int parent = (i-1)/2;
                if (parent >= 0){
                    if (nodes[parent] != null){
                        if (i % 2 == 0){
                            nodes[parent].right = nodes[i];
                        }else {
                            nodes[parent].left = nodes[i];
                        }
                    }
                }
            }
            floor++;
        }
        return nodes[0];
    }

本文所使用的二叉树如下:

在这里插入图片描述

递归

先序遍历
 // 先序
    public void preTravel(TreeNode root){
        if (root == null)
            return;
        System.out.println(root.val);
        preTravel(root.left);
        preTravel(root.right);
    }
中序遍历
// 中序
    public void midTravel(TreeNode root){
        if (root == null)
            return;
        midTravel(root.left);
        System.out.println(root.val);
        midTravel(root.right);
    }
后序遍历
  // 后序
    public void postTravel(TreeNode root){
        if (root == null)
            return;
        postTravel(root.left);
        postTravel(root.left);
        System.out.println(root.val);
    }

迭代

先序遍历

 1. 申请一个新的栈,记为stack。将头节点root压入stack中。
 2.从stack中弹出栈顶节点,记为cur,然后打印cur的值,再将cur的右孩子(不为空的话)先压入stack中,最后将cur的左孩子(不为空的话)压入stack中。
 3. 不断重复步骤2,直到stack为空,全部过程结束。

先序走流程

 节点1先入栈,然后弹出来并打印,将节点3入栈,节点2入栈,从栈顶到栈底依次为2,3。
 节点2弹出来并打印,节点5入栈,节点4入栈,此时栈顶到栈底依次是4,5,3。
 节点4弹出来并打印,节点4没有孩子压入栈,stack从栈顶到栈底依次是5,3。
 节点5弹出来并打印,节点5只有左孩子8入栈,stack从栈顶到栈底依次是8,3。
 节点8弹出来并打印,节点8没有孩子压入栈,stack从栈顶到栈底依次是3。
 节点3弹出来并打印,节点7先入栈,节点6再入栈,stack从栈顶到栈底依次是6,7。
 节点6弹出来并打印,节点6没有孩子压入栈,stack从栈顶到栈底依次是7。
 节点7弹出来并打印,节点6没有孩子压入栈,stack已经为空,过程停止。

打印顺序: 1,2,4,5,8,3,6,7。

代码实现
public void preTravelUnRecur(TreeNode root){
        System.out.println("pre-order");
        if (root != null){
            Stack<TreeNode> stack = new Stack<TreeNode>();
            stack.add(root);
            while (!stack.isEmpty()){
                root = stack.pop();
                System.out.print(root.val+",");
                if (root.right != null)
                    stack.push(root.right);
                if (root.left != null)
                    stack.push(root.left);
            }
        }
        System.out.println("pre-order is end!");
    }
中序遍历
  1. 申请一个新的栈,记为stack。初始时,令变量cur=root。
  2. 先把cur节点压入栈中,以对cur节点为头整颗子树来说,依次把左边界压入栈中,即不停地令cur=cur.left,然后重复步骤2。
  3. 不断重复步骤2,直到发现cur为空,此时从stack中弹出一个节点,记为node。打印node的值,并且让cur=node.right,然后继续重复步骤2。
  4. 当stack为空且cur为空时,整个过程结束。
举例说明流程

  初始时cur节点为1,将节点1压入stack中,令cur=cur.left,即cur变为节点2。
  cur为节点2,将节点2压入stack,令cur=cur.left,即cur变为节点4。
  cur为节点4,将节点4压入stack,令cur=cur.left,即cur变为节点null,此时stack从栈顶到栈底为4,2,1。
  cur为null,从stack弹出节点4(node)并打印,令cur=node.right,即cur变为节点null,此时stack从栈顶到栈底为2,1。
  cur为null,从stack弹出节点2(node)并打印,令cur=node.right,即cur变为节点5,此时stack中栈顶到栈底为1。
  cur为5,将节点5压入stack,令cur = cur.left,即cur=8,此时stack从栈顶到栈底为5,1。
  cur为8,将节点8压入stack,令cur = cur.left,即cur=null,此时stack从栈顶到栈底为8,5,1。
  cur为null,从stack弹出节点8(node)并打印,令cur = node.right,此时stack从栈顶到栈底为5,1。
  cur为null,从stack弹出节点5(node)并打印,令cur = node.right,此时stack从栈顶到栈底为1。
  cur为null,从stack弹出节点1(node)并打印,令cur = node.right,此时stack为空。但是cur并不为空!!
  cur为3,将节点3压入stack,cur=cur.left,即cur变为节点6,此时stack从栈顶到栈底为3.
  cur为6,将节点6压入stack,cur=cur.left,即cur=null,此时stack从栈顶到栈底为6,3.
  cur为null,从stack弹出节点6(node)并打印,cur=node.right,即cur=null,此时stack从栈顶到栈底为3.
  cur为null,从stack弹出节点3(node)并打印,cur=node.right,即cur=7,此时stack为空。
  cur为7,将节点7压入stack,cur=cur.left,即cur=null,此时stack从栈顶到栈底为7。
  cur为null,从stack弹出节7(node)并打印,cur=node.right,即cur=null,此时stack为空.
&emsp;cur=null 并且 stack为空,整个过程停止。

代码实现
public void midTravelUnRecur(TreeNode root){
        System.out.println("in-order:");
        if (root != null){
            Stack<TreeNode> stack = new Stack<>();
            while (!stack.isEmpty() || root != null){
                if (root != null){
                    stack.push(root);
                    root = root.left;
                }else {
                    root = stack.pop();
                    System.out.println(root.val+",");
                    root = root.right;
                }
            }
        }
        System.out.println("in order is end!");
    }
后序遍历

  后序遍历非递归实现稍微有一点麻烦,本文提供两种实现思路。

思路一:两个栈搞定版
  1. 申请一个栈,记为s1,然后将头节点root压入s1中。
  2. 从s1中弹出的节点记为cur,然后依次将cur的左孩子和右孩子压入s1中。
  3. 在整个过程中,每一个从s1中弹出的节点都放进s2中。
  4. 不断重复步骤2和步骤3,直到s1为空,过程停止。
  5. 从s2中依次弹出节点并打印,打印的顺序就是后续遍历的顺序。
举例说明

 节点1放入s1中。
 从s1中弹出节点1,节点1放入s2,然后将节点2和节点3依次放入s1,此时s1从栈顶到栈底为3,2; s2从栈顶到栈底为1。
  从s1中弹出节点3,节点3放入s2, 节点6,7放入s1,此时s1从栈顶到栈底依次为7,6,2; s2依次从栈顶到栈底为3,1。
  从s1中弹出节点7,节点7放入s2, 无节点放入s1,此时s1从栈顶到栈底依次为6,2; s2依次从栈顶到栈底为7,3,1。
  从s1中弹出节点6,节点6放入s2, 无节点放入s1,此时s1从栈顶到栈底依次为2; s2依次从栈顶到栈底为6,7,3,1。
  从s1中弹出节点2,节点2放入s2, 节点4,5放入s1,此时s1从栈顶到栈底依次为4,5; s2依次从栈顶到栈底为2,6,7,3,1。
  从s1中弹出节点5,节点5放入s2, 节点8放入s1,此时s1从栈顶到栈底依次为4,8; s2依次从栈顶到栈底为5,2,6,7,3,1。
  从s1中弹出节点8,节点8放入s2, 无节点放入s1,此时s1从栈顶到栈底依次为4; s2依次从栈顶到栈底为8,5,2,6,7,3,1。
  从s1中弹出节点4,节点4放入s2, 无节点放入s1,此时s1为空; s2依次从栈顶到栈底为4,8,5,2,6,7,3,1。
 s1为空,此时只需要打印s2中的节点即可: 4,8,5,2,6,7,3,1。

  过程可以总结为:每颗子树的头结点都最先从s1中弹出,然后把该节点的孩子节点按照先左再右的顺序压入s1,这时从s1弹出的顺序就是先右再左,所以从s1中弹出的顺序就是中,右,左。 对于s2来说,就是把s1斤进行逆序!!所以s2从栈顶到栈底为左、右、中。

代码实现
// 后序
    // 两个栈版本
    public void postTravel1(TreeNode root){
        System.out.println("post-order: ");
        if (root != null){
            Stack<TreeNode> s1 = new Stack<>();
            Stack<TreeNode> s2 = new Stack<>();

            s1.push(root);

            while (!s1.isEmpty()){
                root = s1.pop();
                s2.push(root);
                if (root.left != null)
                    s1.push(root.left);
                if (root.right != null)
                    s1.push(root.right);
            }

            while (!s2.isEmpty()){
                System.out.println(s2.pop().val + " ");
            }
        }
        System.out.println("post order is end!");
    }
思路二:一个栈搞定版
  1. 申请一个栈,记为stack,将头节点压入stack,同时设置两个变量h和c。在整个流程中,h代表最近一次弹出并打印的节点,c代表stack的栈顶节点,初始时h为头节点,c为null。
  2. 每次令c等于当前stack的栈顶节点,但是不从stack中弹出,这时分三种情况:
    1. 如果c的左孩子不为null,并且h不等于c的左孩子,也不等于c的右孩子,则把c的左孩子压入stack中。原因:h的含义是最近一次弹出并打印的节点,所以如果h等于c的左孩子或者右孩子,说明c的左子树与右子树已经打印完毕,此时不应该再将c的左孩子放入stack中。否则,说明左子树还没有处理过,此时将c的左孩子压入stack中。
    2. 如果条件1不成立,并且c的右孩子不为null,h不等于c的右孩子,则把c的右孩子压入stack中。原因:如果h等于c的右孩子,说明c的右子树已经打印完毕,此时不应该再将c的右孩子放入stack中。否则,说明右子树还没有处理过,此时将c的右孩子压入stack中。
    3. 如果条件1和条件2都不成立,说明c的左子树与右子树都已经打印完毕,那么从stack中弹出c并打印,然后令h=c。
  3. 一直重复步骤2,直到stack为空。
举例说明:

  节点1压入stack中,初始时h为节点1,c为null,stack 从栈顶到栈底为1。
  令c等于stack的栈顶节点——节点1,此时步骤2的条件1命中,将节点2压入stack中,h为节点1,stack从栈顶到栈底依次为2,1。
  令c等于stack的栈顶节点——节点2,此时步骤2的条件1命中,将节点4压入stack中,h为节点1,stack从栈顶到栈底依次为4,2,1。
  令c等于stack的栈顶节点——节点4,此时步骤2的条件3命中,将节点4从stack中弹出并打印,h变为节点4,stack从栈顶到栈底为2,1。
  令c等于stack的栈顶节点——节点2,此时步骤2的条件2命中,将节点5压入stack中,h为节点4,stack从栈顶到栈底依次为5,2,1。
  令c等于stack的栈顶节点——节点5,此时步骤2的条件1命中,将节点8压入stack中,h为节点4,stack从栈顶到栈底依次为8,5,2,1。
  令c等于stack的栈顶节点——节点8,此时步骤2的条件3命中,弹出栈顶节点8,h为节点8,stack从栈顶到栈底依次为8,5,2,1。
  令c等于stack的栈顶节点——节点5,此时步骤2的条件3命中,弹出栈顶节点5,h为节点5,stack从栈顶到栈底依次2,1。
  令c等于stack的栈顶节点——节点2,此时步骤2的条件3命中,弹出栈顶节点2,h为节点2,stack从栈顶到栈底依次1。
  令c等于stack的栈顶节点——节点1,此时步骤2的条件2命中,节点3入stack,h为节点1,stack从栈顶到栈底依次3,1。
  令c等于stack的栈顶节点——节点3,此时步骤2的条件1命中,节点6入stack,h为节点1,stack从栈顶到栈底依次6,3,1。
  令c等于stack的栈顶节点——节点6,此时步骤2的条件3命中,弹出节点6,h为节点6,stack从栈顶到栈底依次3,1。
  令c等于stack的栈顶节点——节点3,此时步骤2的条件2命中,节点7入栈,h为节点6,stack从栈顶到栈底依次7,3,1。
  令c等于stack的栈顶节点——节点7,此时步骤2的条件3命中,节点7弹出,h为节点7,stack从栈顶到栈底依次3,1。
  令c等于stack的栈顶节点——节点3,此时步骤2的条件3命中,节点3弹出,h为节点3,stack从栈顶到栈底依次1。
  令c等于stack的栈顶节点——节点1,此时步骤2的条件3命中,节点1弹出,h为节点1,stack为空。

代码实现
// 一个栈版本
    public void postOrderUnRec2(TreeNode root){
        System.out.println("post-order :  ");
        if (root != null){
            Stack<TreeNode> stack = new Stack<>();
            stack.push(root);
            TreeNode c = null;
            while (!stack.isEmpty()){
                c = stack.peek();
                if (c.left != null && root != c.left && root != c.right)
                    stack.push(c.left);
                else if (c.right != null && root != c.right)
                    stack.push(c.right);
                else{
                    System.out.println(stack.pop().val + " ");
                    root = c;
                }
            }
        }
        System.out.println("post order is end");
    }

到此,二叉树的三种遍历的迭代版本已经描述完毕。但是迭代方法的空间复杂度为O(n),因此有了下面的Mirrors版本遍历,其空间复杂度为O(1)。

Mirrors遍历

Mirrors核心思想

 假设当前来到节点cur,开始时cur来到头结点的位置

  1. 如果cur没有左孩子,cur向右移动(cur = cur.right )

  2. 如果cur有左孩子,找到左子树上最右的节点mostRight:

    a. 如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)
    b. 如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur=cur.right)

  3. cur为空遍历停止

Mirrors遍历的模板代码
//模板
    public void Mirrors(TreeNode root){
        if (root == null)
            return;
        TreeNode cur = root;
        TreeNode mostRight = null;
        while (cur != null){
            mostRight = cur.left;       // mostRight 是cur的左孩子
            if (mostRight != null){     // 有左子树
                while (mostRight.right != null && mostRight.right != cur)
                    mostRight = mostRight.right;
                // mostRight变成了cur左子树上的最右节点
                if (mostRight.right == null){
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }
                else
                    mostRight.right = null;
            }
            cur = cur.right;
        }
    }
先序遍历
按照例子走流程

 这里使用一个list用来表示cur所走过的路径(实际代码实现中不需要,只是为了方便举例)。

 初始时cur = 1, mostRight = 8,节点8的右孩子为空,所以mostRight.right= 1,此时 cur = cur.left;list中的元素为(1);
 cur = 2,mostRight=4,节点4的右孩子为空,所以mostRight.right=2,此时cur = cur.left,list中的元素为(1,2);
 cur=4, cur没有左孩子,因此cur=cur.right。list中的元素为(1,2,4);
 cur=2,mostRight=4,但是mostRight.right不为空,所以执行most.Right=null。cur=cur.right,list中的元素为(1,2,4,2);
 cur=5,mostRight=8, mostRight.right=5,cur=cur.left,list中的元素为(1,2,4,2,5)
 cur=8,cur没有左孩子,cur=cur.right, list中的元素为(1,2,4,2,5,8)
 cur=5, cur有左孩子,左孩子的mostRight.right=cur,所以mostRight.right=null,让cur=cur.right,list中的元素为(1,2,4,2,5,8,5);
 上述中cur此时应该=1,mostRight.right=cur,所以置mostRight.right=null,cur=cur.right,list中的元素为(1,2,4,2,5,8,5,1);
 cur=3,此时mostRight=6,mostRight.right=3,cur=cur.left,list中元素为(1,2,4,2,5,8,5,1,3)
 cur=6,此时cur的左孩子为空,cur=cur.right,list中的元素为(1,2,4,2,5,8,5,1,3,6)
 cur=3,此时mostRight.right=3,所以置,mostRight.right=null,cur=cur.right,list中的元素为(1,2,4,2,5,8,5,1,3,6,3)
 cur=7,此时cur没有左孩子,cur=cur.right,list中的元素为(1,2,4,2,5,8,5,1,3,6,3,7)
 cur=null,结束遍历。

 list中的元素代表cur所走过的路径,与递归得到的先序遍历结果对比:

cur走过的路径:          (1,2,4,2,5,8,5,1,3,6,3,7)
先序遍历递归得到的结果:   (1,2,4,5,8,3,6,7)

 对比可以看出只要是cur有左孩子的地方,其遍历的结果都会显示两遍(上述的1,2,3,5),但是在最终的先序遍历中,只需要打印其第一次遍历到的该节点即可。右孩子只会打印一次不用关心。因此先序遍历的Mirrors代码根据模板可以修改为:

代码实现
// 先序
    public void preOrderOnMirrors(TreeNode root){
        if (root == null)
            return;
        TreeNode cur = root;
        TreeNode mostRight = null;
        while (cur != null){
            mostRight = cur.left;       // mostRight 是cur的左孩子
            if (mostRight != null){     // 有左子树
                while (mostRight.right != null && mostRight.right != cur)
                    mostRight = mostRight.right;
                // mostRight变成了cur左子树上的最右节点
                if (mostRight.right == null){
                    mostRight.right = cur;
                    System.out.print(cur.val + "  ");	// 这里添加打印第一次出现的左孩子
                    cur = cur.left;
                    continue;
                }
                else
                    mostRight.right = null;
            }else
                System.out.print(cur.val + "  ");		// 添加打印右孩子
            cur = cur.right;
        }
    }
中序遍历
按照例子走流程

 流程与先序的一样,根据打印的路径流程与递归中序遍历的结果对比可以看出:

cur走过的路径:          (1,2,4,2,5,8,5,1,3,6,3,7)
中序遍历递归得到的结果:   (4,2,8,5,1,6,3,7)

 对比可以看出只要是cur有左孩子的地方,其遍历的结果都会显示两遍(上述的1,2,3,5),但是在最终的中序遍历中,只需要打印其第二次遍历到的该节点即可。右孩子只会打印一次不用关心。因此中序遍历的Mirrors代码根据模板可以修改为:

代码实现
// 中序
    public void InOrderOnMirrors(TreeNode root){
        if (root == null)
            return;
        TreeNode cur = root;
        TreeNode mostRight = null;
        while (cur != null){
            mostRight = cur.left;       // mostRight 是cur的左孩子
            if (mostRight != null){     // 有左子树
                while (mostRight.right != null && mostRight.right != cur)
                    mostRight = mostRight.right;
                // mostRight变成了cur左子树上的最右节点
                if (mostRight.right == null){
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }
                else {
                    mostRight.right = null;
                    System.out.print(cur.val + "  ");		// 这里代表第二次走到左孩子需要打印
                }
            }else
                System.out.print(cur.val + "  ");		// 右孩子需要打印
            cur = cur.right;
        }
    }
后序遍历
按照例子走流程

 流程与先序的一样,根据打印的路径流程与递归后序遍历的结果对比可以看出:

cur走过的路径:          (1,2,4,2,5,8,5,1,3,6,3,7)
中序遍历递归得到的结果:   (4,8,5,2,6,7,3,1)

 后序遍历与先序、中序均有一些不一样。但是仍然可以发现,规律其实是,当cur第二次走到某节点时,就要该cur的左孩子的所有右分支逆序打印出来。例如:
 当cur第二次来到节点2时,其左孩子只有4,因此打印4。
 当cur第二次来到节点5时,其左孩子只有8,因此打印8。
 当cur第二次来到节点1时,其左孩子的右分支逆序打印应该为5,2。
 当cur第二次来到节点3时,其左孩子只有6,因此打印6。
  最后再添加一次从根节点到右分支的逆序打印,为7,3,1。

代码实现:
// 后序
    public List<Integer> PostOrderOnMirrors(TreeNode root){
        ArrayList<Integer> result = new ArrayList<>();
        if (root == null)
            return result;
        TreeNode cur = root;
        TreeNode mostRight = null;
        while (cur != null){
            mostRight = cur.left;       // mostRight 是cur的左孩子
            if (mostRight != null){     // 有左子树
                while (mostRight.right != null && mostRight.right != cur)
                    mostRight = mostRight.right;
                // mostRight变成了cur左子树上的最右节点
                if (mostRight.right == null){
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }
                else {
                    mostRight.right = null;
                    addPath(result, cur.left);      // 第二次来到左孩子时逆序打印左孩子的右分支
                }
            }
            cur = cur.right;
        }
        addPath(result, root);          // 逆序打印从根节点的右分支
        result.forEach(System.out::println);
        return result;
    }

    private static void addPath(ArrayList<Integer> result, TreeNode root) {
        int count = 0 ;
        while (root != null){
            count++;
            result.add(root.val);
            root = root.right;
        }
        int left = result.size() - count;
        int right = result.size() - 1;
        while (left < right){
            int temp = result.get(left);
            result.set(left, result.get(right));
            result.set(right, temp);
            left++;
            right--;
        }
    }

Mirrors遍历其实与递归的思路相似,只是利用了节点的右指针(如果为空)。这个指针就可以将空间复杂度降低为O(1)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值