每日算法总结——二叉树的Morris遍历

Morris遍历:一种遍历二叉树的方式, 并且时间复杂度 O ( N ) O(N) O(N)额外空间复杂度 O ( 1 ) O(1) O(1),通过利用原树中大量空闲指针的方式,达到节省空间的目的

Morris遍历细节

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

  1. 如果cur没有左孩子,cur向右移动 (cur = cur.right)
  2. 如果cur有左孩子,找到左子树上最右的节点mostRight:
    • 如果mostRight的右指针指向空,让其指向cur,然后cur向左移动 (cur = cur.left)
    • 如果mostRight的右指针指向cur,让其指向null,然后cur向右移动 (cur = cur.right)
  3. cur为空时遍历停止

通常的二叉树遍历是通过栈来完成的,也就是栈记录了之前节点的位置,方便遍历完下面的节点后返回,这种方式是需要占用大量的系统空间的。

而看到Morris遍历过程可知,它是利用了底层树的最右子节点的右孩子指针指向当前节点的方式,记录当前节点的位置,方便在遍历完左子树后返回,然后再遍历右子树

遍历过程举例

在这里插入图片描述

  • 图中橙色圈cur当前指向的节点,橙色路径是指寻找左子树最右子节点的过程蓝色圈是指找到的左子树最右子节点
  • 整个遍历的结果是:1、2、4、2、5、1、3、6、3、7,可以看到有左孩子的节点都被遍历了两次
  • 如何判断当前节点是第几次遍历?
    • 获取到cur的左子树最右节点的右孩子如果是null,则说明是当前节点cur是第一次遍历
    • 获取到cur的左子树最右节点的右孩子如果指向cur,则说明是第二次遍历cur
      • 注意:上图省略了二次查找的过程,如第二次回到节点2时,应当再次寻找节点2的左子树最右节点,此时找到的最右节点的右孩子指向的正是节点2,所以判定是第二次遍历节点2,然后将右孩子指针重新指向null
代码

JavaCode

public class MorrisTraversal {
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            value = data;
        }
    }

    public static void morris(Node root) {
        if (root == null) {
            return;
        }
        Node cur = root;
        Node mostRight = null;
        // 过流程
        while (cur != null) {
            // mostRight是cur左孩子
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    mostRight.right = null;
                }
            }
            cur = cur.right;
        }
    }
}
复杂度估计
  • 空间复杂度肯定是 O ( 1 ) O(1) O(1),因为整个流程中只用到了cur、mostRight两个变量
  • 时间复杂度为 O ( N ) O(N) O(N)为什么
    • Morris遍历细节可知,cur每来到一个新的节点,都会调用寻找左子树最右子节点的过程,如下图:
      在这里插入图片描述

    • 而且每个过程都会调用两遍,因为之前说过cur有可能会有两次遍历。
      比如:

      • cur = 1时,会遍历2、5、11,遍历两遍
      • cur = 2时,会遍历4、9,遍历两遍
    • 所以说遍历右边界的总代价就是 O ( N ) O(N) O(N),整个遍历过程也就是 O ( N + N ) = O ( N ) O(N+N)=O(N) O(N+N)=O(N)

Morris先序遍历
  • 如果当前节点没有左子树,则直接打印
  • 如果当前节点有左子树,且是第一次来到该节点,则打印它;第二次来到不管它
public class MorrisTraversal {
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            value = data;
        }
    }

    public static void preMorris(Node root) {
        if (root == null) {
            return;
        }
        Node cur = root;
        Node mostRight = null;
        // 过流程
        while (cur != null) {
            // mostRight是cur左孩子
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    System.out.print(cur.value + " ");	// Processing
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    mostRight.right = null;
                }
            } else {
                System.out.print(cur.value + " ");	// Processing
            }
            cur = cur.right;
        }
    }
}
Morris中序遍历
  • 如果当前节点没有左子树,则直接打印
  • 如果当前节点有左子树,第一次来到该节点,不管它;第二次来到该节点,打印
public class MorrisTraversal {
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            value = data;
        }
    }

    public static void preMorris(Node root) {
        if (root == null) {
            return;
        }
        Node cur = root;
        Node mostRight = null;
        // 过流程
        while (cur != null) {
            // mostRight是cur左孩子
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    mostRight.right = null;
                }
            }
            System.out.print(cur.value + " ");	// Processing
            cur = cur.right;
        }
    }
}
Morris后序遍历
  • 寻找左子树最右子节点的过程,将左子树的右边界记录下来
  • 第二次来到该节点时,逆序打印左子树的右边界
  • 最后单独逆序打印整棵树的右边界
public class MorrisTraversal {
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            value = data;
        }
    }

    

    /**
     * Morris 后序遍历
     */
    public static void posMorris(Node root) {
        if (root == null) {
            return;
        }
        Node cur = root;
        Node mostRight = null;
        while (cur != null) {
            mostRight = cur.left;
            if (mostRight != null) {
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    mostRight.right = null;
                    printRightEdge(cur);
                }
            }
            cur = cur.right;
        }
        printRightEdge(root);
        System.out.println();
    }

    /**
     * 以x为头的树,逆序打印这棵树的右边界
     */
    public static void printRightEdge(Node x) {
        Node tail = reverseRightEdge(x);
        Node cur = tail;
        while (cur != null) {
            System.out.println(cur.value + " ");
            cur = cur.right;
        }
        reverseRightEdge(tail);
    }

    /**
     * 右边界反转
     */
    public static Node reverseRightEdge(Node from) {
        Node pre = null;
        Node next = null;
        while (from != null) {
            next = from.right;
            from.right = pre;
            pre = from;
            from = next;
        }
        return pre;
    }
}
题目应用

如何判断一棵树是搜索二叉树?

  • 思路就是中序遍历整棵二叉树,然后判断遍历的结果是否是升序

  • 利用Morris中序遍历,可以达到空间复杂度 O ( 1 ) O(1) O(1)

/**
 * 判断一棵树是否是搜索二叉树
 */
public static boolean isBst(Node root) {
    if (root == null) {
        return false;
    }
    Node cur = root;
    Node mostRight = null;
    int preValue = Integer.MIN_VALUE;
    // 过流程
    while (cur != null) {
        // mostRight是cur左孩子
        mostRight = cur.left;
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            if (mostRight.right == null) {
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else {
                mostRight.right = null;
            }
        }
        if (preValue >= cur.value) {
            return false;
        }
        preValue = cur.value;
        cur = cur.right;
    }
    return true;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值