浅谈迭代(非递归)遍历二叉树

二叉树,序列结构,本质上等同于链表,后者也可看做单叉树、直树。不分叉有一个好处,就是前进方向是唯一、固定的。反过来讲,分叉的麻烦之处就是要走回头路。而对于单向的结构而言,就得想办法保存该信息。若在二叉树结点内再维护一个prev结点,变成双向结构,则遍历问题会变得相当容易。不过这样做,也仅是将实现算法时需要的额外空间,转移到了原始数据结构中,代价仍旧是存在的。当然,并不是每走一步,都要用空间保存回程信息,我们仅需在走到尽头时,能够跳转回即可。因此,理论上可以实现 O 1 O_{1} O1的空间复杂度。

  1. 不管是迭代遍历还是递归遍历,先序遍历或是中、后序遍历,二叉树遍历都只做了三件事:处理左子树,处理右子树,处理当前结点

  2. 依据处理当前结点的时机不同,可分为:先序、中序、后序三种遍历方式。此外,还有层序遍历的方式,即按层输出结点(可利用队列实现)。

  3. 二叉树迭代遍历的主要难点在于:如何在处理完子树后,返回父结点(或右结点,总之,这里强调的是“返回”)。常用思路是利用栈先入后出的特性。

  4. 经典思路一:仿照层序遍历的方式,可利用,遍历结点,同时压左右非空子结点入栈不同的是,栈在处理栈顶结点时,又会压新栈,使得旧结点(亦即右结点)被打压。而队列的层序遍历方式,先到先得(FIFO)。

    由于先序遍历可立即处理完当前结点,故可使用该方法。而对于中序、后序遍历,处理完子树后,还要返回处理父结点。因此只能采用其他方法。(此处可行性的判断属于个人猜测)

    fine,找到反例了。考察父结点与子结点输出的相对顺序,不难发现后序遍历是先序遍历的逆序,而子结点间,只要将左右结点顺序交换下即可。这就解决了必须先处理子结点才能处理当前结点的矛盾,因此可以将保存结果的数据结构换成链表,将每次输出插入到头部(而非尾部)即可。(栈还是要用的,因为必须要保存之前经过的结点)。(又:这种方法并没有实际的后序遍历,而是打印出与后序遍历一样的结果,属于取巧的做法)

    对应 preOrderNormalTraverse,postOrderNormalTraverse。

  5. 经典思路二:核心思想与思路一相同,只是对流程进一步简化。思路一中,不断压新栈的结果就是,不断向左遍历且保存右结点。因此,我们对每结点,压栈并遍历至最左结点,而后依次出栈转至右结点(也可以是父结点)。换句话讲,我们可将右节点视为下一层的左节点。对每结点沿左臂压栈并遍历至其尽头。(访问路径呈从右上至左下的对角线,每次跳至右结点,也就是开始下一级的对角线遍历)

    对应 preOrderDiagonalTraverse_V1,preOrderDiagonalTraverse_V2,inOrderDiagonalTraverse,postOrderDiagonalTraverse。

  6. 经典思路三:无论是递归遍历还是以上所说的遍历方式,其空间、时间复杂度都是 O N O_{N} ON。我们引入额外空间的根本原因还是为了保存父结点,以便回档。但实际上还有一种更巧妙方式,我们可以在进入左子树前,可利用树结构的特点,将子树中的某一子结点的子结点指向父结点,这样就能保存父结点的地址,实现 O 1 O_{1} O1的空间复杂度。

    对应 preOrderMorrisTraverse,inOrderMorrisTraverse,postOrderMorrisTraverse。

  7. 即所谓 Morris Traversal

  8. 该方法的核心思想是,处理父结点(亦即当前结点)的左子树前先将左子树的最右结点的右结点(此时为null)指向父结点(传送门),再处理左子树。这样当我们处理完左子树后,即可顺便从最右结点的右结点返回(因为最右结点即为左子树处理流程的最后一个结点)。这种做法,每结点最多访问两次(一次要设立传送门,一次要实际遍历处理),因此时间复杂度是 O N O_{N} ON

    这里有个隐藏问题,为什么要在左子树的终结点设立传送门,而不是右子树?答案很简单,无论是先中后序遍历,不考虑父结点的处理时机的话,都要先处理左子树,再处理右子树。即,在处理完左子树后,必须要能回到父结点。这就是为何要对左子树特殊处理的原因。更进一步,在进行后序遍历时,处理完右子树后,还要再返回父结点,那为何没给右子树设立传送门呢?因为后序遍历的特殊性,我们采用了另一种操作:从更高一级的角度看,由于右子树与父结点都在爷爷结点的左子树内,因此在处理完右子树,通过末端传送门返回爷爷结点后,将余下未处理的对角线支路逆序处理即可。而对于最顶端结点,由于其没有父结点(导致子树没有爷爷结点),故须人为引入dummyHead,将其左结点指向原最顶端结点。(事实上,从次一级的角度看,右子树可以看作是有传送门的,只不过它返回的不是父结点,而是爷爷结点)

附上代码:

import java.util.*;

public class BTreeTraversal {
   
    static class TreeNode {
   
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode() {
   }
        TreeNode(int val) {
    this.val = val; }
        TreeNode(int val, TreeNode left, TreeNode right) {
   
            this.val = val;
            this.left = left;
            this.right = right;
        }

        TreeNode createBTreeFromArray(Integer[] arr) {
   
            if(arr.length == 0){
   
                return null;
            }
            TreeNode root = new TreeNode(arr[0]);
            Queue<TreeNode> queue = new LinkedList<>();
            queue.add(root);
            boolean isLeft = true;
            for(int i = 1; i < arr.length; i++){
   
                TreeNode node = queue.peek();
                // for each arr[i], they will be processed by if and else branches separately.
                if(isLeft){
   
                    if(arr[i] != null){
   
                        node

你可能感兴趣的:(数据结构与算法,JAVA,二叉树,数据结构,算法)