Morris遍历

512 阅读7分钟

写在前面

笔试不推荐用,容易出错,平常写就好

面试推荐用,增加印象分

其他:Morris方法中间过程会改变原树结点的指向,虽然最后都可以改回来,但是题中如果要求过程中不可以改变树的走向,那么Morris方法就不可以用了

综述

一种遍历二叉树的方式,并且时间复杂度是O(N), 额外空间复杂度是O(1)

通过利用原树中大量空闲指针的方式,达到节省空间的目的。

Morris遍历

Morris遍历细节

假设来到当前节点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为空时遍历停止

Morris遍历图解

/////////////////////////////////////////////////////////////////////////// image.png /////////////////////////////////////////////////////////////////////////// image.png /////////////////////////////////////////////////////////////////////////// image.png /////////////////////////////////////////////////////////////////////////// image.png

等等

cur依次出现的顺序是:1 2 4 2 5 1 3 6 3 7

相关分析

每一个有左子树的节点都会出现两次, 比如 1 2 3

如果一个结点有左树,怎样判断是第几次回到自己

答:通过自己左子树的最右节点指向谁来确定,如果指向null,就是第一次,如果指向自己就是第二次。

相关代码

    //定义结点结构
    public static class Node {
        public int value;
        public Node left;
        public Node right;

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

    public static void morris(Node head){
        if (head == null){
            return;
        }
        //当前cur先来到head节点
        Node cur = head;
        //承载是否有左子树和有左子树的情况下左子树最右侧的节点
        Node mostRight = null;
        while(cur != null){//符合上面morris的流程,直到cur是null才停止
            mostRight = cur.left;//mostRight是cur左孩子
            if (mostRight != null){//有左子树
                //右侧不为空就进行下去,如果右侧不是当前节点进行下去,符合上面morris的流程
                while (mostRight.right != null && mostRight.right != cur){
                    mostRight = mostRight.right;
                }
                //如果当前节点左子树最右侧节点的右侧是null
                if (mostRight.right == null){
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    //如果当前节点左子树最右侧节点的右侧是cur
                    mostRight.right = null;
                }
            }
            cur = cur.right;
        }
    }

复杂度分析

每来到一个节点就会遍历一下它的左孩子的右边界,这个会不会使时间复杂度上升呢?

答:因为每个需要遍历右边界的节点,遍历的节点都是不同的,所以时间复杂度不会变化是O(N)。 image.png

  • 故空间复杂度是O(1),时间复杂度是O(N)。

Morris改前序遍历

如果一个节点只出现过一次直接打印,如果出现过两次,打印第一次的

相关代码:

    //定义结点结构
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }
    //改先序遍历
    public static void morris(Node head){
        if (head == null){
            return;
        }
        //当前cur先来到head节点
        Node cur = head;
        //承载是否有左子树和有左子树的情况下左子树最右侧的节点
        Node mostRight = null;
        while(cur != null){//符合上面morris的流程,直到cur是null才停止
            mostRight = cur.left;//mostRight是cur左孩子
            if (mostRight != null){//有左子树
                //右侧不为空就进行下去,如果右侧不是当前节点进行下去,符合上面morris的流程
                while (mostRight.right != null && mostRight.right != cur){
                    mostRight = mostRight.right;
                }
                //如果当前节点左子树最右侧节点的右侧是null
                if (mostRight.right == null){//这是第一次来到cur
                    //打印第一次出现的
                    //---------------------先序添加下面------------
                    System.out.println(cur.value);
                    //---------------------先序添加上面------------
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    //mostRight.right == cur
                    //这是第二次来到cur
                    //如果当前节点左子树最右侧节点的右侧是cur
                    mostRight.right = null;
                }
                //---------------------先序添加下面------------
            }else{
                //没有左子树的情况
                System.out.println(cur.value);
            }
            //---------------------先序添加上面------------
            cur = cur.right;
        }
    }

Morris改中序遍历

如果一个节点只出现过一次直接打印,如果出现过两次,打印第二次的

相关代码:

    //定义结点结构
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }
    //改中序遍历
    public static void morris(Node head){
        if (head == null){
            return;
        }
        //当前cur先来到head节点
        Node cur = head;
        //承载是否有左子树和有左子树的情况下左子树最右侧的节点
        Node mostRight = null;
        while(cur != null){//符合上面morris的流程,直到cur是null才停止
            mostRight = cur.left;//mostRight是cur左孩子
            if (mostRight != null){//有左子树
                //右侧不为空就进行下去,如果右侧不是当前节点进行下去,符合上面morris的流程
                while (mostRight.right != null && mostRight.right != cur){
                    mostRight = mostRight.right;
                }
                //如果当前节点左子树最右侧节点的右侧是null
                if (mostRight.right == null){//这是第一次来到cur
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    //mostRight.right == cur
                    //这是第二次来到cur
                    //如果当前节点左子树最右侧节点的右侧是cur
                    mostRight.right = null;
                }
            }
            //---------------------中序添加下面------------
            System.out.println(cur.value);
            //---------------------中序添加上面------------
            cur = cur.right;
        }
    }

Morris改后序遍历

先只看出现两次的节点,在这个节点出现第二次的时候,逆序打印它的左子树的右边界,等过完了所有出现两次的节点,逆序打印整棵树的右边界

图像分析

image.png

相关代码:

    //定义结点结构
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }
    //改后序遍历
    public static void morris(Node head){
        if (head == null){
            return;
        }
        //当前cur先来到head节点
        Node cur = head;
        //承载是否有左子树和有左子树的情况下左子树最右侧的节点
        Node mostRight = null;
        while(cur != null){//符合上面morris的流程,直到cur是null才停止
            mostRight = cur.left;//mostRight是cur左孩子
            if (mostRight != null){//有左子树
                //右侧不为空就进行下去,如果右侧不是当前节点进行下去,符合上面morris的流程
                while (mostRight.right != null && mostRight.right != cur){
                    mostRight = mostRight.right;
                }
                //如果当前节点左子树最右侧节点的右侧是null
                if (mostRight.right == null){//这是第一次来到cur
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    //mostRight.right == cur
                    //这是第二次来到cur
                    //如果当前节点左子树最右侧节点的右侧是cur
                    mostRight.right = null;
                    //---------------------后序添加下面------------
                    //逆序打印左树的右边界
                    printEdge(cur.left);
                    //---------------------后序添加上面------------
                }
            }
            cur = cur.right;
        }
        //整个while跑完后,单独打印整个树的右边界
        printEdge(head);
        System.out.println();
    }
    
    //以X为头的树,逆序打印这棵树的右边界
    public static void printEdge(Node X) {
        Node tail = reverseEdge(X);
        Node cur = tail;
        while(cur != null){
            System.out.println(cur.value + " ");
            cur = cur.right;
        }
        //最后在反转回去,毕竟我们不能改变树的走向(最终),有些题,任何时候都不可以
        //那么morris这个方法都不能用了
        reverseEdge(tail);
    }
    
    //向链表一样的反转指向,从而方便逆向打印
    public static Node reverseEdge(Node from) {
        Node pre = null;
        Node next = null;
        while(from != null){
            next = from.right;
            from.right = pre;
            pre = from;
            from = next;
        }
        return pre;
    }

Morris改二叉搜素树

二叉搜索树定义:中序遍历之后形成的序列是递增的,注意不可以有相等的情况。

相关代码

    //定义结点结构
    public static class Node {
        public int value;
        public Node left;
        public Node right;

        public Node(int data) {
            this.value = data;
        }
    }
    //改二叉搜索树
    public static boolean isBST(Node head){
        if (head == null){
            return true;
        }
        //当前cur先来到head节点
        Node cur = head;
        //承载是否有左子树和有左子树的情况下左子树最右侧的节点
        Node mostRight = null;
        //---------------------二叉搜索树添加下面------------
        int preValue = Integer.MIN_VALUE;
        //---------------------二叉搜索树添加上面------------
        while(cur != null){//符合上面morris的流程,直到cur是null才停止
            mostRight = cur.left;//mostRight是cur左孩子
            if (mostRight != null){//有左子树
                //右侧不为空就进行下去,如果右侧不是当前节点进行下去,符合上面morris的流程
                while (mostRight.right != null && mostRight.right != cur){
                    mostRight = mostRight.right;
                }
                //如果当前节点左子树最右侧节点的右侧是null
                if (mostRight.right == null){//这是第一次来到cur
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                }else {
                    //mostRight.right == cur
                    //这是第二次来到cur
                    //如果当前节点左子树最右侧节点的右侧是cur
                    mostRight.right = null;
                }
            }
            //---------------------二叉搜索树添加下面------------
            //从中序上改,也是这个位置
            if(cur.value <= preValue){
               return false;
            }
            preValue = cur.value;
            //---------------------二叉搜索树添加上面------------
            cur = cur.right;
        }
        return true;
    }

    //以X为头的树,逆序打印这棵树的右边界
    public static void printEdge(Node X) {
        Node tail = reverseEdge(X);
        Node cur = tail;
        while(cur != null){
            System.out.println(cur.value + " ");
            cur = cur.right;
        }
        //最后在反转回去,毕竟我们不能改变树的走向(最终),有些题,任何时候都不可以
        //那么morris这个方法都不能用了
        reverseEdge(tail);
    }

    //向链表一样的反转指向,从而方便逆向打印
    public static Node reverseEdge(Node from) {
        Node pre = null;
        Node next = null;
        while(from != null){
            next = from.right;
            from.right = pre;
            pre = from;
            from = next;
        }
        return pre;
    }

相关分析

这样的解法,不用将数据收集到list之后,在比较是不是递增的,解决了最本质的遍历的问题,所以很多问题,Morris都是最优解。

dp套路与Morris

因为Morris方法解决了最本质的遍历的问题,所以很多问题,Morris都是最优解。

dp套路链接

那么那些方法是以dp套路是最优解,那些是以Morris是最优解?

答:

  • 如果你发现你的方法必须做第三次信息的强整合,就是dp套路最优解。
  • 如果不需要,最优解就是Morris遍历