二叉树

二叉树的Morris遍历

2021-01-06  本文已影响0人  铜炉

前言

前面已经进行过二叉树的递归遍历和迭代遍历,在前两种遍历中,二叉树遍历的空间复杂度都是O(n),这是因为不管是递归遍历还是迭代遍历,都用了栈空间来帮我们维护调用关系,方便我们在走到端点的时候可以回退。

那么,换一个思路想想,我们可不可以不用栈,而是通过记录下节点要回退的位置,帮助我们直接退回到目标节点呢?这个回退的指针我们又该怎么维护呢?可以发现,二叉树的叶子节点的左右指针都为空,而我们需要回退的位置也是在叶子节点触发,那么我们考虑可以将叶子节点的指针指向目标节点,这样也就不需要额外的栈空间了。

而事实上,Morris遍历之所以能将二叉树的空间复杂度降为O(1),也是这么做的。

Morris遍历的实现原则

1、如果cur无左孩子,cur向右移动(cur=cur.right)
2、 如果cur有左孩子,找到cur左子树上最右的节点,记为mostright
1)如果mostright的right指针指向空,让其指向cur,cur向左移动(cur=cur.left)
2)如果mostright的right指针指向cur,让其指向空,cur向右移动(cur=cur.right)

以上就是Morris遍历的规则,其中mostright的节点,即为节点在中序遍历中的前序节点。
Morris遍历的本质就是会遍历两次节点,一个快指针,一个慢指针,慢指针到达一个节点的时候,快指针就走到当前节点的左子树的最右节点,将该节点的右指针指向慢指针的位置,然后慢指针再移动到下一步。如此往复,等回退的时候,就可以通过之前便利过程中建立的指针关系迅速回到目标节点位置。

Morris前序遍历
接下来,先看一下Morris前序遍历的实现。

public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        if (root == null) {
            return res;
        }
        // 慢指针
        TreeNode cur = root;
        // 快指针,寻找cur的中序遍历的前序节点
        TreeNode mostright = null;
        
        while (cur != null) {
            // 快指针移动到左子树
            mostright = cur.left;
            
            if (mostright != null) {
                // 左子树不为空,快指针移动到左子树的最右节点,即cur的中序遍历的前序节点
                // 此处注意不能走过头,如果最右节点已经标记过,就停在最右节点处
                while (mostright.right != null && mostright.right != cur) {
                    mostright = mostright.right;
                }
                
                if (mostright.right == null) {
                    // 如果是最右节点还没有和当前节点建立关系,就建立关系,
                    // 因为是前序遍历,cur指针在移动之前都要记录
                    res.add(cur.val);
                    mostright.right = cur;
                    cur = cur.left;
                    continue;
                } else {
                    // 逻辑到这里,说明本次遍历到的最右节点已经和当前节点建立了关系,本次是第二次遍历到cur所在的节点,所以把关系清除.
                    mostright.right = null;
                }
            } else {
                // 此时的mostright在左节点,如果左节点为空,cur不用向左走了,直接记录下来
                res.add(cur.val);
            }
            // 到这里两种情况,左子树为空,且当前节点已经记录,往右走
            // cur指针到达了某个节点的左子树的最右节点,回到那个节点
            cur = cur.right;
        }
        return res;

    }

Morris中序遍历

在前序遍历的基础上,中序遍历需要改动一下记录cur节点的触发时机

public List<Integer> inorderTraversal(TreeNode root) {
            List<Integer> res = new ArrayList<Integer>();
            if (root == null) {
                return res;
            }
            // 慢指针
            TreeNode cur = root;
            // 快指针,寻找cur的中序遍历的前序节点
            TreeNode mostright = null;

            while (cur != null) {
                // 快指针移动到左子树
                mostright = cur.left;

                if (mostright != null) {
                    // 左子树不为空,快指针移动到左子树的最右节点,即cur的中序遍历的前序节点
                    // 此处注意不能走过头,如果最右节点已经标记过,就停在最右节点处
                    while (mostright.right != null && mostright.right != cur) {
                        mostright = mostright.right;
                    }

                    if (mostright.right == null) {
                        // 如果是最右节点还没有和当前节点建立关系,就建立关系,
                        mostright.right = cur;
                        cur = cur.left;
                        continue;
                    } else {
                        // 中序遍历,左子树遍历完成,记录当前节点
                        res.add(cur.val);
                        // 逻辑到这里,说明本次遍历到的最右节点已经和当前节点建立了关系,本次是第二次遍历到cur所在的节点,左子树已经遍历完成,所以把关系清除.
                        mostright.right = null;
                    }
                } else {
                    // 此时的mostright在左节点,如果左节点为空,cur不用向左走了,直接记录下来
                    res.add(cur.val);
                }
                // 到这里两种情况,左子树为空,且当前节点已经记录,往右走
                // cur指针到达了某个节点的左子树的最右节点,回到那个节点
                cur = cur.right;
            }
            return res;
    }
上一篇下一篇

猜你喜欢

热点阅读