二叉树遍历系列02-Morris遍历

二叉树遍历系列02-Morris遍历

一、引言

Morris遍历是一种时间复杂度为O(N),空间复杂度为O(1)的二叉树遍历方法,Morris遍历是一种迭代遍历,Morris遍历不需要借助额外的栈空间,只需要进行普通的循环迭代即可完成整棵二叉树的遍历,因为Morris通过利用结点空指针,完成了遍历的转移(十分巧妙的想法)

二、递归序

在二叉树遍历系列01,中讲解了二叉树遍历的递归序,这是一个非常重要的概念,因为这可以帮助我们更好的理解Morris遍历在遍历过程中对先序序列和中序序列以及后序序列的加工(在遍历的过程中何时处理结点)

三、Morris遍历算法简介

Morris的先序遍历、中序遍历以及后序遍历的算法框架基本上是一致的,区别在于对于每一个结点的处理时间,Morris遍历正是在不同时刻完成对结点的处理,从而加工出了,不同的遍历序列(先序、中序、后序)

3.1 Morris遍历过程简介

  1. 设置游标指针cur = root,如果cur == null,转2,否则转3
  2. 返回先序遍历结果
  3. 如果cur有左孩子,转4,否则转5
  4. 设置mostRight = cur.left,并且让mostRight一直往右走,即mostRight = mostRight.right,直到mostRight.right == null 或者mostRight.right == cur为止,转6
  5. cur = cur.right
  6. 如果cur的最右结点的右孩子为null,那么将mostRight.right = cur,cur = cur.left,否则转7
  7. most.right = null,转5

代码实现:

private void morris(TreeNode<V> root) {
    TreeNode<V> cur = root;
    while (cur != null) {
        if (cur.left != null) {
            TreeNode<V> mostRight = cur.left;
            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;
    }
}

下图展示了一个完整的Morris遍历:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z4Jau5i4-1650450818791)(E:\西城风雨楼-技术栈学习笔记\算法与数据结构笔记\4.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oZjtYOk6-1650450818791)(E:\西城风雨楼-技术栈学习笔记\算法与数据结构笔记\5.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ObuNkJ9-1650450818793)(E:\西城风雨楼-技术栈学习笔记\算法与数据结构笔记\6.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c16yVLkN-1650450818793)(E:\西城风雨楼-技术栈学习笔记\算法与数据结构笔记\7.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zuK0tKFw-1650450818794)(E:\西城风雨楼-技术栈学习笔记\算法与数据结构笔记\8.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eD4yQi9X-1650450818794)(E:\西城风雨楼-技术栈学习笔记\算法与数据结构笔记\9.jpg)]
在这里插入图片描述

我们将cur指针指向的数字分别记录下来:

1、2、4、7、4、2、5、1、3、6

我们会发现Morris遍历的遍历序列中存在两种类型的结点

(1)在遍历序列中只出现了一次的结点,这种结点一定没有左孩子

(2)在遍历序列中出现了两次的结点,这种结点一定有左孩子

3.2 基于Morris遍历加工出先序序列

上述例子中的Morris序列为:

1、2、4、7、4、2、5、1、3、6

真实二叉树的先序序列为:

1、2、4、7、5、3、6

这两个序列的区别是:

出现了一次的结点,那么直接记录,出现了两次的结点只在第一次出现的时候记录,那么很明显我们需要在上述的代码框架中添加一些记录操作,用来记录我们需要的结点,那么我们怎么知道哪些结点是第一次出现,哪些结点会出现两次,并且怎么在第一次出现的时候记录呢?

现在来回答这个问题:

只出现一次的结点一定没有左孩子,根据这个特点,很容易知道我们应该在哪里记录,cur.left为null的时候,cur就应该被记录,具体位置参考下面代码:

根据Morris遍历的算法,我们可以知道,cur第一次到达一个结点的时候,如果cur有左孩子,那么算法就会去找该左孩子的最右结点,如果cur是第一次被访问,那么算法会将该最右结点的右孩子指针(此时为null),重定向到cur。当cur在继续遍历的过程中,通过它的right回到某个祖先结点时,那么这个祖先结点就是第二次被访问的时候,对应代码中的位置也如下,先序遍历,我们需要记录第一次到达的位置:

private void morris(TreeNode<V> root) {
    TreeNode<V> cur = root;
    while (cur != null) {
        if (cur.left != null) {
            TreeNode<V> mostRight = cur.left;
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            if (mostRight.right == null) {
                // 这个if分支是cur第一次被访问的时候,因为mostRight.right为null
                // 如果cur是第二次被访问,那么cur一定是通过这个mostRight.right回来的
                // mostRight.right不可能为null,只可能是指向cur
                System.out.println(cur.val);
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else {
                // 这里发现cur的most.right为cur,那么说明这是第二次回到cur
                // 并且cur就是通过mostRight.right回来的,所以才会发现mostRight.right为cur
                mostRight.right = null;
            }
        } else {
            // 在原有代码的基础上添加这个else分支
            // 这个else对应的就是cur.left == null的情况
            // 对于先序序列,我们应该将其记录,这里以打印表示记录
            System.out.println(cur.val);
        } 
        cur = cur.right;
    }
}

3.3 基于Morris遍历加工出中序序列

有了先序序列的加工经验,加工中序序列就显得非常容易了,还是先看Morris遍历的序列:

1、2、4、7、4、2、5、1、3、6

二叉树的中序序列:

7、4、2、5、1、3、6

这两个序列的区别是:

如果一个结点在Morris序列中只出现一次,那么中序序列将会记录,如果在Morris序列中出现两次,那么中序序列只会在该结点第二次出现时记录

有了先序序列的分析,我们已经清楚的掌握了,何时记录只出现一次的数字,何时在数字第一次出现时记录,何时在数字出现第二次时记录,具体看代码:

private void morris(TreeNode<V> root) {
    TreeNode<V> cur = root;
    while (cur != null) {
        if (cur.left != null) {
            TreeNode<V> mostRight = cur.left;
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            if (mostRight.right == null) {
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else {
                System.out.println(cur.val);
                mostRight.right = null;
            }
        } else {
            System.out.println(cur.val);
        } 
        cur = cur.right;
    }
}

3.4 基于Morris遍历加工出后序序列

在Morris序列中加工出后序序列比前两种都要麻烦,但是殊途同归,先看Morris序列:

1、2、4、7、4、2、5、1、3、6

再看后序序列:

7、4、5、2、6、3、1

直接观察比较困难,这里直接给出结论:

从Morris序列加工出后序序列只需要关注那些在Morris遍历中出现了两次的顶点,对于每一个出现了两次的顶点,在第二次访问它时,将它的左子树以right指针逆序输出(看一下code就明白了,比如第二次回到1的时候,将5、2加入后序序列),遍历结束后将这个操作对root结点也做一次

private void postOrderUseMorris(TreeNode<V> root, StringBuilder post) {
    TreeNode<V> cur = root;
    while (cur != null) {
        if (cur.left != null) {
            TreeNode<V> mostRight = cur.left;
            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;
                // 第二次到达的时候,需要处理左孩子,逆序收集
                TreeNode<V> reverse = reverseWithRight(cur.left);
                TreeNode<V> visit = reverse;
                while (visit != null) {
                    post.append(visit.value).append("\t");
                    visit = visit.right;
                }
                reverseWithRight(reverse);
            }
        }
        cur = cur.right;
    }
    TreeNode<V> reverse = reverseWithRight(root);
    cur = reverse;
    while (cur != null) {
        post.append(cur.value).append("\t");
        cur = cur.right;
    }
    reverseWithRight(reverse);
}

/**
     * 将root的以右孩子进行反转
     *
     * @param node 结点
     * @return 返回反转之后的结点
     */
private TreeNode<V> reverseWithRight(TreeNode<V> node) {
    TreeNode<V> cur = node;
    TreeNode<V> next;
    TreeNode<V> pre = null;

    while (cur != null) {
        next = cur.right;
        cur.right = pre;
        pre = cur;
        cur = next;
    }

    return pre;
}

四、Morris遍历代码完整实现

package binarytreevisit;


/**
 * @author 西城风雨楼
 */
public class BinaryTree2<V> {
    private TreeNode<V> root;

    public String preOrderRecur() {
        StringBuilder pre = new StringBuilder();
        preOrder(root, pre);
        return pre.toString();
    }

    public String inOrderRecur() {
        StringBuilder in = new StringBuilder();
        inOrder(root, in);
        return in.toString();
    }

    public String postOrderRecur() {
        StringBuilder post = new StringBuilder();
        postOrder(root, post);
        return post.toString();
    }

    private void preOrder(TreeNode<V> root, StringBuilder pre) {
        if (root == null) {
            return;
        }
        pre.append(root.value).append("\t");
        preOrder(root.left, pre);
        preOrder(root.right, pre);
    }

    private void inOrder(TreeNode<V> root, StringBuilder in) {
        if (root == null) {
            return;
        }
        inOrder(root.left, in);
        in.append(root.value).append("\t");
        inOrder(root.right, in);
    }

    private void postOrder(TreeNode<V> root, StringBuilder post) {
        if (root == null) {
            return;
        }
        postOrder(root.left, post);
        postOrder(root.right, post);
        post.append(root.value).append("\t");
    }

    public String inOrderMorris() {
        StringBuilder in = new StringBuilder();
        inOrderUseMorris(root, in);
        return in.toString();
    }

    public String postOrderMorris() {
        StringBuilder post = new StringBuilder();
        postOrderUseMorris(root, post);
        return post.toString();
    }

    public String preOrderMorris() {
        StringBuilder pre = new StringBuilder();
        preOrderUseMorris(root, pre);
        return pre.toString();
    }

    private void inOrderUseMorris(TreeNode<V> root, StringBuilder in) {
        TreeNode<V> cur = root;
        while (cur != null) {
            if (cur.left != null) {
                TreeNode<V> mostRight = cur.left;
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    in.append(cur.value).append("\t");
                    mostRight.right = null;
                }
            } else {
                in.append(cur.value).append("\t");
            }
            cur = cur.right;
        }
    }

    private void preOrderUseMorris(TreeNode<V> root, StringBuilder pre) {
        TreeNode<V> cur = root;
        while (cur != null) {
            if (cur.left != null) {
                TreeNode<V> mostRight = cur.left;
                while (mostRight.right != null && mostRight.right != cur) {
                    mostRight = mostRight.right;
                }
                if (mostRight.right == null) {
                    pre.append(cur.value).append("\t");
                    mostRight.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    mostRight.right = null;
                }
            } else {
                pre.append(cur.value).append("\t");
            }
            cur = cur.right;
        }
    }

    private void postOrderUseMorris(TreeNode<V> root, StringBuilder post) {
        TreeNode<V> cur = root;
        while (cur != null) {
            if (cur.left != null) {
                TreeNode<V> mostRight = cur.left;
                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;
                    // 第二次到达的时候,需要处理左孩子,逆序收集
                    TreeNode<V> reverse = reverseWithRight(cur.left);
                    TreeNode<V> visit = reverse;
                    while (visit != null) {
                        post.append(visit.value).append("\t");
                        visit = visit.right;
                    }
                    reverseWithRight(reverse);
                }
            }
            cur = cur.right;
        }
        TreeNode<V> reverse = reverseWithRight(root);
        cur = reverse;
        while (cur != null) {
            post.append(cur.value).append("\t");
            cur = cur.right;
        }
        reverseWithRight(reverse);
    }

    /**
     * 将root的以右孩子进行反转
     *
     * @param node 结点
     * @return 返回反转之后的结点
     */
    private TreeNode<V> reverseWithRight(TreeNode<V> node) {
        TreeNode<V> cur = node;
        TreeNode<V> next;
        TreeNode<V> pre = null;

        while (cur != null) {
            next = cur.right;
            cur.right = pre;
            pre = cur;
            cur = next;
        }

        return pre;
    }

    /**
     * 根据先序和中序重建二叉树
     *
     * @param pre 先序
     * @param mid 中序
     */
    public void rebuild(V[] pre, V[] mid) {
        root = rebuildHelper(pre, 0, pre.length - 1,
                             mid, 0, mid.length - 1);
    }

    /**
     * 根据先序和中序重建二叉树,函数不保证先序和中序序列的合法性
     * 以及是否匹配
     *
     * @param pre      先序
     * @param preLeft  先序起始位置
     * @param preRight 先序终止位置
     * @param mid      中序
     * @param midLeft  中序起始位置
     * @param midRight 中序终止位置
     * @return 返回重建的二叉树的根节点
     */
    private TreeNode<V> rebuildHelper(V[] pre,
                                      int preLeft,
                                      int preRight,
                                      V[] mid,
                                      int midLeft,
                                      int midRight) {
        if (preLeft > preRight) {
            return null;
        }

        TreeNode<V> root = new TreeNode<>(pre[preLeft]);
        if (preLeft == preRight) {
            return root;
        }

        // 找到左右子树的分界位置
        int leftBorder = Integer.MIN_VALUE;
        for (int i = midLeft; i <= midRight; i++) {
            if (root.value.equals(mid[i])) {
                leftBorder = i;
                break;
            }
        }
        // 这里border可能找不到
        if (leftBorder == Integer.MIN_VALUE) {
            throw new RuntimeException("序列错误");
        }
        // 求出左子树的大小
        int leftChildSize = leftBorder - midLeft;
        // 递归创建左子树
        root.left = rebuildHelper(pre, preLeft + 1, preLeft + leftChildSize,
                                  mid, midLeft, leftBorder - 1);
        // 递归创建右子树
        root.right = rebuildHelper(pre, preLeft + 1 + leftChildSize, preRight,
                                   mid, leftBorder + 1, midRight);
        return root;
    }

    private static class TreeNode<V> {
        public V value;
        public TreeNode<V> left;
        public TreeNode<V> right;

        public TreeNode(V value, TreeNode<V> left, TreeNode<V> right) {
            this.value = value;
            this.left = left;
            this.right = right;
        }

        public TreeNode(V value) {
            this.value = value;
        }
    }

    public static void main(String[] args) {
        Integer[] pre = new Integer[]{1, 2, 4, 8, 9, 5, 3, 6, 10, 7};
        Integer[] in = new Integer[]{8, 4, 9, 2, 5, 1, 10, 6, 3, 7};
        BinaryTree2<Integer> tree = new BinaryTree2<>();
        tree.rebuild(pre, in);
        System.out.println("先序:");
        System.out.println(tree.preOrderRecur());
        System.out.println(tree.preOrderMorris());
        System.out.println("中序:");
        System.out.println(tree.inOrderRecur());
        System.out.println(tree.inOrderMorris());
        System.out.println("后序:");
        System.out.println(tree.postOrderRecur());
        System.out.println(tree.postOrderMorris());
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值