二叉树的深度优先遍历之先序中序后序递归非递归Morris遍历全解

递归版本

二叉树的深度优先遍历,包括:
1.前序遍历
2.中序遍历
3.后序遍历

他们是如何定义的呢?
见代码:

public void traverse(TreeNode root) {
    if (root == null) {
        return;
    }
    // 先序,preOrder,第一次到达该节点处
    res.get(0).add(root.val);
    traverse(root.left);

    // 中序,inOrder第二次到达该节点处
    res.get(1).add(root.val);
    traverse(root.right);

    // 后序,postOrder第三次到达该节点处
    res.get(2).add(root.val);
}

如代码所示:

  1. 先序遍历是刚走到该节点便获取该节点的值,然后递归遍历左子树、右子树(中左右);
  2. 中序遍历是递归遍历完左子树,然后获取该节点的值,最后再递归遍历右子树(左中右);
  3. 后序遍历是递归遍历完左子树,再递归遍历右子树,最后获取该节点的值(左右中);

注:上述代码中res的声明为:List<List<Integer>> res;

如图所示:
递归版本遍历

图中描述了递归在树的各个节点之间的抽象过程:
图形说明:

  1. 箭头指向表示递归函数访问到该节点;
  2. N表示null节点,指向null节点的指针,表示递归函数进入到null节点的判断处。

结合递归函数看该图就很简单了:

  1. 先序遍历,就是第一个箭头指向时打印的地方(中左右,从上往下,刚进入到该节点就获取该节点的值)
  2. 中序遍历,就是第二个箭头指向时打印的地方(左中右,遍历左边节点返回,然后再获取该节点的值,然后访问右节点)
  3. 后序遍历,就是第三个箭头指向时打印的地方(左右中,遍历完成左、右两边节点后返回,最后再获取该节点的值)

非递归版本遍历的统一模板

前边图形描述了递归版本的过程,非递归版本即模拟该过程即可:

详见代码:

public void nonRecursiveAllInOne(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    // 后序遍历,上一次访问的节点
    TreeNode pre = null;
    Deque<TreeNode> stack = new ArrayDeque<>();
    while (!stack.isEmpty() || cur != null) {
        // 一直往左边走
        while (cur != null) {
            // 先序遍历,刚访问该节点就获取该节点的值
            res.get(0).add(cur.val);
            stack.push(cur);
            cur = cur.left;
        }
        // 一直往左边走后,cur最后会为空
        // 此时获取压入栈中的元素(没弹出)
        cur = stack.peek();
        // 如果当前节点的右节点是前一次访问的节点,那么是第三次回来了,就是后序遍历
        // 如果当前节点的右节点是空,因为我们代码一直往左边走,所以右边的null节点是没法访问到的(而且右边的null节点也没必要访问),此时,中序遍历和后序遍历都在此处了
        if (cur.right == pre || cur.right == null) {
            if (cur.right == null) {
                // 如果cur.right是null,那么中序遍历获取结果
                res.get(1).add(cur.val);
            }
            // 后序获取结果
            res.get(2).add(cur.val);
            // 后序遍历,栈中元素可以弹出了,因为不需要再回来了
            stack.pop();
            // 更新后序遍历上一次访问的节点
            pre = cur;
            // cur要置空,因为要返回上一层,即取栈中元素
            cur = null;
        } else {
            // 第二次访问,中序遍历
            res.get(1).add(cur.val);
            cur = cur.right;
        }
    }
}

非递归版本的过程见图:
非递归版本遍历

cur.right 并未被真正的访问到,或者说 cur.right == null 通过是否是null的判断,实际上也算一次访问了。

非递归版本遍历的简化

统一的模板是适用于所有的递归情况,如果只需要单独的某个版本是可以简化代码逻辑的。
如果是先序和中序遍历,那么只需要关心第一次访问和第二次访问即可,无需关心其他;而后序遍历比较麻烦,需要关心是否是第三次返回的情况,所以各个非递归遍历简化版代码如下:

public void preOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    Deque<TreeNode> stack = new ArrayDeque<>();
    while (!stack.isEmpty() || cur != null) {
        //
        while (cur != null) {
            // 先序遍历,第一次访问该节点
            res.get(0).add(cur.val);
            stack.push(cur);
            cur = cur.left;
        }
        // cur 此时为null
        // 返回,即弹出栈中元素覆盖即可
        cur = stack.pop();
        // 如果right节点是null也无妨,会继续弹出
        cur = cur.right;
    }
}
public void inOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    Deque<TreeNode> stack = new ArrayDeque<>();
    while (!stack.isEmpty() || cur != null) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        cur = stack.pop();
        // 中序遍历,第二次访问该节点
        res.get(1).add(cur.val);
        cur = cur.right;
    }
}
public void postOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    TreeNode pre = null;
    Deque<TreeNode> stack = new ArrayDeque<>();
    while (!stack.isEmpty() || cur != null) {
        while (cur != null) {
            stack.push(cur);
            cur = cur.left;
        }
        // 后序遍历比较麻烦,不能直接弹出,需要判断是否是第三次返回
        cur = stack.peek();
        if (cur.right == pre || cur.right == null) {
            res.get(2).add(cur.val);
            stack.pop();
            pre = cur;
            cur = null;
        } else {
            cur = cur.right;
        }
    }
}

Morris遍历

在有了对树的遍历的流程的认识后,简单介绍一下二叉树的Morris遍历。
正如我们所见,二叉树的遍历,需要用到栈,即空间复杂度是O(logn),时间复杂度是O(n).
Morris遍历可以实现二叉树空间复杂度为O(1),时间复杂度也是O(n),他的原理是通过利用原树中大量空闲指针的方式,达到节省空间的目的。

其基本的规则是:
假设访问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 为空时遍历停止

根据规则,我们可以看到,通过 mostRight 节点的right指针,判断是第几次回到当前节点:

  1. 如果是指向null,那么说明是第一次来到该节点;
  2. 如果指向自己,那么说明是第二次回到当前节点。

Morris遍历并没有第三次回到当前节点的操作,所以需要逆序打印边界。

时间复杂度是O(N),因为对于树中的每个节点至多只能被访问两次。

遍历过程详情图和代码:

public class Morris {
    public static void main(String[] args) {

        TreeNode root = new TreeNode(4);
        root.left = new TreeNode(2);
        root.right = new TreeNode(6);
        root.left.left = new TreeNode(1);
        root.left.right = new TreeNode(3);
        root.right.left = new TreeNode(5);
        root.right.right = new TreeNode(7);
        traverseTree(root);
        System.out.println("=====");
        morris(root);
    }

    public static void traverseTree(TreeNode root) {
        if (root == null) {
            return;
        }
        // 先序
//        System.out.println(root.val);
        traverseTree(root.left);
        // 中序
//        System.out.println(root.val);
        traverseTree(root.right);
        // 后序
        System.out.println(root.val);
    }

    public static void morris(TreeNode root) {
        if (root == null) {
            return;
        }
        TreeNode cur = root;
        while (cur != null) {
            // 1. 没有左孩子,cur向右边移动
            if (cur.left == null) {
                // 这个也是第一次到,也是第二次到,对于先序和中序遍历而言,都需要处理
//                System.out.println(cur.val);
                cur = cur.right;
            } else {
                // 2 有左孩子,找到左孩子最右的非空节点 mostRight
                TreeNode mostRight = cur.left;
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                // 判断 mostRight
                if (mostRight.right == null) {
                    // a. mostRight 指向null,则让其指向自己,向左边走
                    mostRight.right = cur;
                    // 第一次到cur,先序遍历
//                    System.out.println(cur.val);
                    cur = cur.left;
                } else {
                    // b. 如果 mostRight 的右指针指向 cur,让其指向 null,然后cur向右移动
                    mostRight.right = null;
                    // 第二次到cur,中序遍历
//                    System.out.println(cur.val);

                    // 后序遍历,左右中,相当于是打印树的 \ 这个样子的右斜线,所以在第二次离开的时候,逆序打印右斜线 \ 即可。
                    // 为了使用O(1)的空间复杂度,可以先对树的节点进行 "反转链表" 的操作,然后打印完成之后,再反转回来
                    printReverseRightEdge(cur.left);
                    cur = cur.right;
                }
            }
        }
        printReverseRightEdge(root);
    }

    private static void printReverseRightEdge(TreeNode root) {
        if (root == null) {
            return;
        }
        // 反转列表
        TreeNode rHead = reverseRightEdge(root);
        // 打印
        TreeNode cur = rHead;
        while (cur != null) {
            System.out.println(cur.val);
            cur = cur.right;
        }
        // 反转回来
        reverseRightEdge(rHead);
    }

    private static TreeNode reverseRightEdge(TreeNode root) {
        TreeNode pre = null;
        TreeNode cur = root;
        while (cur != null) {
            TreeNode next = cur.right;
            cur.right = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
}

参考资料

Morris 遍历图,参考:https://blog.csdn.net/qq_38636076/article/details/119147902
牛客网左神算法课程

posted @   zhenjiaguo  阅读(130)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示