ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

一篇文章看完怎么遍历二叉树(递归,迭代,Morris)!

2022-05-21 11:00:45  阅读:135  来源: 互联网

标签:right cur 迭代 mostRight Morris 二叉树 null root 节点


二叉树数据结构

public class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
}

理解递归序

如何用递归遍历一颗二叉树
image

    public void recursion(TreeNode root) {
        if (root != null) {
            recursion(root.left);
            recursion(root.right);
        }
    }

image

上图为执行代码后的走向,我们从根节点1开始访问,依次访问1 -> 2 -> 4 -> 4 -> 4 -> 2 -> 5 -> 5 -> 5 -> 2 -> 1 -> 3 -> 6 -> 6 -> 6 -> 3 -> 7 -> 7 -> 7 -> 3 -> 1

可以发现每个节点都进入了3次,上面这串访问的顺序就是递归序

先序,中序,后序的递归写法,既是分别在第1,2,3次进入的时候,进行需要的操作,如打印,添加到集合之类的

public void recursion(TreeNode root) {
        if (root != null) {
            //首次进入
            recursion(root.left);
            //遍历完左子树后第二次进入
            recursion(root.right);
            //遍历完右子树后第三次进入
        }
}

image

迭代方法实现遍历

递归的写法十分简单,因为递归相当于隐式维护了一个栈,而迭代则需要我们显式的维护一个栈,那么对于前中后序遍历,我们显然需要制定不同的规则来实现

前序遍历

规则如下:

  1. 弹出就打印
  2. 有右压右
  3. 有左压左
    public void pre2(TreeNode root) {
        if (root != null) {
            LinkedList<TreeNode> stack = new LinkedList<>();
            //头节点不为空,先将头节点入栈
            stack.push(root);
            while (!stack.isEmpty()) {
                //弹出就打印
                TreeNode node = stack.pop();
                System.out.println(node.val);
                //右节点不为null就压进stack
                if (node.right != null) {
                    stack.push(node.right);
                }
                //左节点不为null就压进stack
                if (node.left != null) {
                    stack.push(node.left);
                }
            }
        }
    }

中序遍历

规则如下:

  1. 整条左边界依次进栈
  2. 当1无法继续时,弹出节点就打印,然后去弹出节点的右树继续执行1
public void in(TreeNode root) {
        if (root != null) {
            LinkedList<TreeNode> stack = new LinkedList<>();
            while(!stack.isEmpty() || root != null) {
                if (root != null) {
                    stack.push(root);
                    root = root.left;
                } else {
                    root = stack.pop();
                    System.out.println(root.val);
                    root = root.right;
                }
            }
        }
    }

整棵树被所有的左边界分解,对于每一颗子树,都是先分解完它的左子树,然后再头,再右

后序遍历

后序遍历要根据左,右,根的顺序遍历,

观察先序遍历,发现先序是根,左,右的顺序,而迭代的写法是先入右再入左,那么如果先入左再入右,可以得到一个根,右,左的顺序,发现倒过来便是后序遍历

所以首先可以使用两个栈来完成后序遍历,就是先序遍历的改版

    public void post(TreeNode root) {
        if (root != null) {
            LinkedList<TreeNode> stack = new LinkedList<>();
            //辅助我们反转用的栈
            LinkedList<TreeNode> stack2 = new LinkedList<>();
            //头节点不为空,先将头节点入栈
            stack.push(root);
            while (!stack.isEmpty()) {
                //弹出就入辅助栈
                TreeNode node = stack.pop();
                stack2.push(node);
                //先序是先有再左,所以这边先左后右
                if (node.left != null) {
                    stack.push(node.left);
                }
                if (node.right != null) {
                    stack.push(node.right);
                }
            }
            while (!stack2.isEmpty()) {
                System.out.print(stack2.pop().val + " ");
            }
        }
    }

当然还可以通过一个栈完成整个后序遍历,我们需要定义一个指针h,指向已经被我们搞过的节点,什么意思,结合代码和图一起看:

    public void post2(TreeNode h) {
        if (h != null) {
            LinkedList<TreeNode> stack = new LinkedList<>();
            stack.push(h);
            TreeNode c = null;
            while (!stack.isEmpty()) {
                //当前栈顶的节点
                c = stack.peekFirst();
                //如果当前节点有左孩子,并且左孩子和有孩子都没有被我们看过,那么就把左孩子入栈
                if (c.left != null && h != c.left && h != c.right) {
                    stack.push(c.left);
                    
                //否则如果当前节点有右孩子,并且还没右被我们看过,那么就把右孩子入栈
                //为什么这边不用判断左孩子有没有被看过? 因为走到这一步,一定是上一步判断左孩子被看过了,或者根本就没有左孩子,才会到这,后序遍历嘛,看完左就要看右,最后才是根
                } else if (c.right != null && h != c.right) {
                    stack.push(c.right);
                    
                //如果当前节点的左右孩子都被看过了,那么好,该轮到当前节点了
                } else {
                    
                    //弹出就打印,用h标记已经被我们搞过的节点
                    System.out.println(stack.removeFirst().val);
                    h = c;
                }
            }
        }
    }

image

总体思路就是用一个h一直标记已经被我们记录的节点,cur表示当前节点,如果h是cur的左孩子,那么说明cur的左节点已经被遍历过了,那么就该遍历右子树了,如果h是cur的右孩子,说明cur的左右子树都遍历过了,轮到cur了,整个流程走完就完成了后序遍历

理解Morris序

递归和非递归都需要维护一个栈来实现遍历(非手动和手动的区别)因此空间复杂度都是O(N)级别的。

Morris遍历树可以在维持时间复杂度仍然是O(N)的前提下,实现O(1)的空间复杂度

什么是Morris序?

Morris遍历首先需要定义两个指针,一个cur指向当前节点,一个mostRight指向当前节点左子树的右边界的最右节点

什么是右边界最右节点,就是一直mostRight = mostRight.right 直到 mostRight.right == null为止,此时的mostRight就是最右节点,如下图所示

image

图中节点1的左子树最右边界为5,这个很好理解

然后Morris遍历需要遵循以下两条规则:

  1. 当cur的左孩子为null时,cur = cur.right
  2. 当cur的左孩子不为null时,如果我们是第一次进入cur指针指向的节点,那么先找到mostRight,将mostRight的右孩子指向cur,cur = cur.left, 如果是第二次进入cur指针指向的节点,那么先找到mostRight,将mostRight的右孩子还原为null,cur = cur.right

第一次看不懂不要紧,先跟着图过一遍,如下图所示

image

整理以下得到Morris序:1 -> 2 -> 4 -> 2 -> 5 -> 1 -> 3 -> 6 -> 3 -> 7

只要是有左子树的节点,都会进入两次,无左子树的节点只会进入一次(因为一进去会判断cur.left是不是空,根据条件1,为null直接cur = cur.right了)

可以看到,由于没有栈记录访问过和没访问过的节点,我们利用树的空闲指针,即mostRight的右孩子,每当访问一个节点时,先让mostRight指向cur,然后先往左走,当第二次再回到cur的时候,我们就知道这是第二次访问cur了,该往右走了,因此吧之前改过的右孩子重新指向null,cur往右走,如此往复,即可遍历整棵树。

    public void morris(TreeNode root) {
        if (root != null) {
            //指向当前访问的节点
            TreeNode cur = root;
            //指向cur左子树的右边界最右节点
            TreeNode mostRight = null;
            //当cur跑到null的时候停止
            while (cur != null) {
                mostRight = cur.left;
                //如果cur有左子树
                if (mostRight != null) {
                    //让mostRight一直往右跑,有两种情况停止,一种是mostRight.right == null,还有可能我们之前改过mostRight.right == cur
                    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序输出一颗二叉树的先序,中序,后序序列呢

image

每当第一次访问节点时就打印,打印出来的就是先序序列

public void morrisPre(TreeNode root) {
        if (root != null) {
            TreeNode cur = root;
            TreeNode 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) {
                        //第一次访问节点就打印
                        System.out.println(cur.val);
                        mostRight.right = cur;
                        cur = cur.left;
                        continue;
                    } else {
                        mostRight.right = null;
                    }
                } else {
                    //对于没有左子树的节点也是,第一次访问就打印
                    System.out.println(cur.val);
                }
                cur = cur.right;
            }
        }
    }

而中序遍历,就是对于没有左子树的节点第一访问就打印,有左子树的节点第二次访问再打印

public void morrisIn(TreeNode root) {
        if (root != null) {
            TreeNode cur = root;
            TreeNode 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;
                    }
                }
                //第一次访问有左子树的节点那边会continue掉,所以执行到这里要么是第二次访问有左子树的节点,要么直接访问的就是没有左子树的节点
                System.out.println(cur.val);
                cur = cur.right;
            }
        }
    }

那么后序怎么实现呢?

后序的改编与先序与中序略有不同

我们需要在第二次访问无左子树节点时,倒着打印他左子树的右边界,在整棵树遍历完之后,倒着打印整棵树的右边界,

image

如图所示,第二次访问2时,打印4,第二次访问1时,打印5, 2,第二次访问3时,打印6,最后打印7, 3, 1

合起来就是后序遍历的结果:4, 5, 2, 6, 7, 3, 1

public void morrisPost(TreeNode root) {
        if (root != null) {
            TreeNode cur = root;
            TreeNode 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;
                        //第二次访问时倒序打印右边界
                        printEdge(cur.left);
                    }
                }

                cur = cur.right;
            }
            //最后打印整棵树的右边界
            printEdge(root);
        }
}

//倒序打印右边界
public void printEdge(TreeNode head) {
    	//先把右边界反转了
        TreeNode tail = reverseEdge(head);
        TreeNode cur = tail;
    	//打印
        while (cur != null) {
            System.out.println(cur.val + " ");
            cur = cur.right;
        }
    	//再翻回去
        reverseEdge(tail);
}

//反转右边界,就跟反转链表一样
public TreeNode reverseEdge(TreeNode from) {
        TreeNode pre = null;
        TreeNode next = null;
        while (from != null) {
            next = from.right;
            from.right = pre;
            pre = from;
            from = next;
        }
        return pre;
}

标签:right,cur,迭代,mostRight,Morris,二叉树,null,root,节点
来源: https://www.cnblogs.com/bue1v/p/16294638.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有