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

二叉树,序列结构,本质上等同于链表,后者也可看做单叉树、直树。不分叉有一个好处,就是前进方向是唯一、固定的。反过来讲,分叉的麻烦之处就是要走回头路。而对于单向的结构而言,就得想办法保存该信息。若在二叉树结点内再维护一个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.left = new TreeNode(arr[i]);
                        queue.add(node.left);
                    }
                    isLeft = false;
                }
                else {
                    if(arr[i] != null){
                        node.right = new TreeNode(arr[i]);
                        queue.add(node.right);
                    }
                    queue.remove();
                    isLeft = true;
                }
            }
            return root;
        }
    }
    void preOrderNormalTraverse(TreeNode root){
        // this way of traverse only suited for preOrder.
        // because in the while-loop, we first pop and then push, which
        // means we MUST process the cur node RIGHT AWAY.
        // however, for inOrder and postOrder, we have to process child nodes before cur node.
        //      fine, the core idea can also be used for postOrder.
        //      preOrder: mid->left->right.     postOrder: left->right->mid.
        //      just REVERSE preOrder and SWAP left and right, we will get the postOrder.
        //      and it would cost more space than the current version.
        // T: O(n), S: O(n)
        if(root==null)
            return;
        List<Integer> tmp = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while(!stack.isEmpty()){
            root = stack.pop();
            tmp.add(root.val);
            if(root.right!=null) stack.push(root.right);
            if(root.left!=null) stack.push(root.left);
        }
        for(Integer e : tmp){
            System.out.print(e + " ");
        }
    }
    void preOrderDiagonalTraverse_V1(TreeNode root){
        // this way of traverse only suited for preOrder.
        // because in the while-loop, we first pop and then push, which
        // means we MUST process the cur node RIGHT AWAY.
        // however, for inOrder and postOrder, we have to process child nodes before cur node.
        // T: O(n), S: O(n)
        if(root==null)
            return;
        List<Integer> tmp = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while(!stack.isEmpty()){
            root = stack.pop();
            while(root!=null){
                tmp.add(root.val);
                if(root.right!=null) stack.push(root.right);//store NonNull right node.
                root = root.left;//if left==null, while-loop end and root get updated.
            }
        }
        for(Integer e : tmp){
            System.out.print(e + " ");
        }
    }
    void preOrderDiagonalTraverse_V2(TreeNode root){
        // this way is suited for inOrder and postOrder.
        // because in the while-loop, we first push and then pop.
        // T: O(n), S: O(n)
        List<Integer> tmp = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        while(root!=null || !stack.isEmpty()){
            // the condition of !stack.isEmpty() is easy to understand.
            // and the root!=null condition:
            //  1.in case of root is null at the beginning;
            //  2.the right node may be nonNull while the stack is empty,
            //      which means in our hands, there is a new right subtree to visit.
            //      in this situation, just limited with !stack.isEmpty() condition will make us miss it.
            while(root!=null){
                tmp.add(root.val);
//                if(root.right!=null)
//                    stack.push(root.right);//store NonNull right node.
//                //since it's preOrder and the father node has been processed, we can store
//                //    nonNull right node rather than father node. In this situation,
//                //    the pop operation MUST be limited with !stack.isEmpty(). However, if we
//                //    choose to store father node, then it's NO NEED to limit the pop operation.
//                //    Because there MUST be a father node for each left node.
                stack.push(root);
                root = root.left;
            }
//            if(!stack.isEmpty()) root = stack.pop();
            root = stack.pop();
            root = root.right;
        }
        for(Integer e : tmp){
            System.out.print(e + " ");
        }
    }
    void preOrderMorrisTraverse(TreeNode root){
        // T: O(n), S: O(1)
        List<Integer> tmp = new ArrayList<>();
        TreeNode cur, prev;
        cur=root;
        while(cur!=null){
            if(cur.left==null){
                tmp.add(cur.val);
                //directly output cur node since it's preOrder.
                cur=cur.right;
                //jump to right subtree. If it's portal, we will pass it and meet the old father node,
                //  Then, else-branch in below will be satisfied AGAIN, but this time, the else-branch
                //  in that else-branch will be useful.
            }
            else{
                prev=cur.left;
                while(prev.right!=null && prev.right!=cur)
                    //search for the predecessor, i.e., the most right node in left subtree.
                    //on the way back, we WOULD STILL get in this route AGAIN. the
                    //prev.right!=cur condition will AVOID the endless loop and indicates which
                    //node the predecessor is.
                    prev=prev.right;
                if(prev.right==null){
                    //first time to predecessor. go deep into the left subtree layer by layer.
                    prev.right=cur;     //set flag.
                    tmp.add(cur.val);   //output cur node.
                    cur=cur.left;       //go to next left subtree since the flag is set and we get a portal.
                }
                else{
                    //second time to predecessor. flag is set, so the left subtree is processed,
                    //it's on the way back.
                    prev.right=null;    //remove flag since we have pass the portal.
                    cur=cur.right;      //go to the right subtree. A new world is waiting for us.
                }
            }
        }
        for(Integer e : tmp){
            System.out.print(e + " ");
        }
    }

    void inOrderDiagonalTraverse(TreeNode root){
        // detailed explanation for this algorithm framework is issued in
        // the void preOrderDiagonalTraverse_V2(TreeNode root) method, which is also
        // included in current class.
        // T: O(n), S: O(n)
        List<Integer> tmp = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        while(root!=null || !stack.isEmpty()){
            while(root!=null){
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            tmp.add(root.val);
            root= root.right;
        }
        for(Integer e : tmp){
            System.out.print(e + " ");
        }
    }
    void inOrderMorrisTraverse(TreeNode root){
        // T: O(n), S: O(1)
        List<Integer> tmp = new ArrayList<>();
        TreeNode cur, prev;
        cur = root;
        while(cur!=null){
            if(cur.left==null){
                tmp.add(cur.val);
                cur = cur.right;
            }
            else {
                prev = cur.left;
                while(prev.right!=null && prev.right!=cur){
                    prev = prev.right;
                }
                if(prev.right==null){
                    prev.right=cur;
                    cur = cur.left;
                }
                else{
                    prev.right=null;
                    tmp.add(cur.val);
                    cur = cur.right;
                }
            }
        }
        for(Integer e : tmp){
            System.out.print(e + " ");
        }
    }

    void postOrderNormalTraverse(TreeNode root){
        // NOTE that here we use LinkedList to use the addFirst() method easily.
        // and LinkedList needs MORE SPACE than ArrayList.
        // Strictly speaking, this method didn't TRAVERSE in a postOrder. It just PRINTS.
        // T: O(n), S: O(n)
        LinkedList<Integer> tmp = new LinkedList<>();
        if(root==null)
            return;
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while(!stack.isEmpty()){
            root = stack.pop();
            tmp.addFirst(root.val);
            if(root.left!=null) stack.push(root.left);
            //left first in last out
            if(root.right!=null) stack.push(root.right);
        }
        for(Integer e : tmp){
            System.out.print(e + " ");
        }
    }
    void postOrderDiagonalTraverse(TreeNode root){
        // detailed explanation for this algorithm framework is issued in
        // the void preOrderDiagonalTraverse_V2(TreeNode root) method, which is also
        // included in current class.
        // T: O(n), S: O(n)
        List<Integer> tmp = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        TreeNode visitedNode = null;
        while(root!=null || !stack.isEmpty()){
            while(root!=null){
                stack.push(root);
                root = root.left;
            }
            root = stack.pop(); //peek() is more suit, here we use pop() to get a unified frameworks.
            if(root.right==null || root.right==visitedNode){
                //postOrder is MORE COMPLICATED, we need a FLAG to tell us whether we HAVE PROCESSED the right node and
                //we are on the way BACK when we pop the father node.
                tmp.add(root.val);
                visitedNode = root; //ESSENTIAL code!!! NEXT STEP will meet father node, so we should update visitedNode.
                root = null;    //ESSENTIAL code!!! SKIP the NEXT while(root!=null) loop to POP the father of root.
            }
            else{
                stack.push(root);
                root = root.right;  //enter into right tree.
            }
        }
        for(Integer e : tmp){
            System.out.print(e + " ");
        }
    }
    void postOrderMorrisTraverse(TreeNode root){
        // T: O(n), S: O(1)
        //FINE, postOrderMorrisTraverse is INGENIOUS. Still, the problem is how to print the
        //  left and right subtree when we meet father node. S: O(1) means we can't use stack or
        //  other space. The only method is to TRAVERSE AGAIN when we ARE SURE that we HAVE PROCESSED
        //  left and right subtree. Hence, printReverse() and reverseLinkedList() are useful.
        //  Particularly, dummyHead is introduced to be the FATHER of original TOP node.
        List<Integer> tmp = new ArrayList<>();
        TreeNode cur, prev, dummyHead;
        dummyHead = new TreeNode();
        dummyHead.left = root;
        cur = dummyHead;
        while(cur!=null){
            if(cur.left==null){
                cur = cur.right;
            }
            else {
                prev = cur.left;
                while(prev.right!=null && prev.right!=cur){
                    prev = prev.right;
                }
                if(prev.right==null){
                    prev.right=cur;
                    cur = cur.left;
                }
                else{
                    prev.right=null; // it's time to REVERSELY print the diagonal.
                    printReversely(cur.left, prev, tmp);
                    cur = cur.right;
                }
            }
        }
        for(Integer e : tmp){
            System.out.print(e + " ");
        }
    }

    void printReversely(TreeNode from, TreeNode to, List<Integer> tmp){
        reverseLinkedList(from, to);
        TreeNode cur = to;
        while(cur!=null){
            tmp.add(cur.val);
            cur=cur.right;
        }
        reverseLinkedList(to, from);
    }
    void reverseLinkedList(TreeNode from, TreeNode to){
        TreeNode pre=null, cur=from, next=null;
        while(true){
            next=cur.right;
            cur.right=pre;
            pre=cur;
            cur=next;
            if(pre==to)
                break;
        }
    }

    public static void main(String[] args) {
        Integer[] arr = new Integer[]{1,null,2,3,4,5,6,null,7,null,null,null,null,null,8};
        TreeNode head = new TreeNode().createBTreeFromArray(arr);
        BTreeTraversal test = new BTreeTraversal();
        test.postOrderMorrisTraverse(head);
    }
}

参考资料:

  1. Morris Traversal 方法遍历二叉树
  2. 二叉树的后序遍历——迭代算法
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值