经典教程:二叉树前、中、后遍历详解【递归+迭代+morris】

https://www.cnblogs.com/BlueBlueSea/p/13888630.html

 

一、思路

1、先序

1)递归法

 DLR,先输出,两个递归分别传参左右孩子

 

2)迭代法:一般思路

1、一开始根进栈,栈不空进入循环。

2、pop,输出。

3、孩子进栈,顺序右左。出栈左右。

4、栈空,结束循环。

 

竟然用栈来实现,而且是先右后左!我惊呆了!!!

我一直以为是队列。。。。

转念想,java的先序递归,本质就是用栈帮你实现呀!

 

【别人思路】

while(root){

//打印本结点

System.out.print(root.data+"  ");

if(T1){

            打印T1

         }

        if(T2){

            打印T2

         }

}

原文链接:https://blog.csdn.net/hhhghh_/article/details/104793656

 

 

3Morris前序遍历【迭代】

中序改成前序。与中序唯一的不同之处,是在加索引前输出。

 

【注意】

Morris学习顺序:中序-前序-后序进栈顺序右,左。出栈左右。

Morris思想:看中序。

 

 

2、中序

(1)递归法

 LDR,中间输出

 

(2)迭代法

1、左链入栈:先把左子树全部入栈,直到最左边的叶子。

2、栈不空时,弹出一个节点,输出val。

3、无论栈空不空,每次都要对右孩子继续做“左链入栈”。

4、循环退出条件,stack空且当前节点为空。

 

 

常见的错误版本:【我最开始看的教程】

(1)https://blog.csdn.net/yangfeisc/article/details/44497429

(2)https://mp.weixin.qq.com/s?__biz=MzU0ODMyNDk0Mw==&mid=2247487028&idx=1&sn=e06a0cd5760e62890e60e43a279a472b&chksm=fb419d14cc36140257eb220aaeac182287b10c3cab5c803ebd54013ee3fc120d693067c2e960&scene=21#wechat_redirect

错误位置:【绿框】

改错:不应该是直接把if去掉,而是把tree=tree.right拿到if外面!也就是说,无论栈是否空,都要往右走。

 

 

 

3Morris中序遍历【原版】

我的理解:

1、一直往左走,每次左不空就先标索引(左子树最右节点为pre),往左走。cur=cur.lc。

2、直到左为空,就把cur输出,

如果cur不是叶子,就是往右子树走。cur=cur.rc。

如果cur是叶子,就是根据索引返回父节点。仍然是cur=cur.rc。 

3、返回父节点后,再查一次pre,发现pre.rc=cur,则输出当前父亲(cur),并且把索引删掉。

然后就遍历右子树啦。cur=cur.rc。

 

2个地方输出:输出后都是往右走,因为输出的是中。

左空,输出cur,往右走/返回父亲,都是cur=cur.rc。

左不空,且pre.rc==cur,输出cur,往右走cur=cur.rc。

 

往左走:

只要左不空就标索引pre.rc=cur,标完索引往左走cur=cur.lc。

 

 

步骤:

1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。

2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。

a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。

b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。

3. 重复以上1、2直到当前节点为空。

 

 

 

 

 

时间复杂度:O(n)

关键点:查索引时,直觉上复杂度为O(nlgn),实际上,最多走两遍边,O(2n),也就是O(n)。

 

空间复杂度为O(1):不用栈而是借助叶子的右指针不费空间,只用到了一个pre用O(1)。

时间复杂度是O(n):

  每次查询所谓“前驱节点”其实就是找到左子树的最右节点,每个节点都要查两遍左子树,一遍是加索引,一遍是删索引。

  看似涉及树高度,是O(nlgn),实际上是每条边最多走两遍,是O(2n)也就是O(n)。

 

 

 

3、后序

 

1)递归法

 LRD。没意思,略。

 

2)迭代法

与先序迭代的区别:

1、输出时,新的要插到前面。这是为了保证后序,最后输出根。

2、左右子树入栈顺序,是先左后右。因为插到前面顺序正好与先序反着,先序是先右后左。

 

3Morris后序遍历

与中序2个不同点:

1、dump.lc = root:

     *  cur从虚拟节点dump开始,而不是从root开始。。

     *  这是为了把最后一个节点输出,否则会剩下最右节点无法输出,绝妙。

     *  (不加dump最右节点无法输出,加上dump,dump无法输出,相当于往后赶了一步)

2、输出:大顺序小逆序,从cur_lc到pre的逆序

     *  在删除索引后输出从左子树到pre整个路径的逆序,其他地方均不输出。

     *  index保证,小序列内部逆序,但整体还是顺序往大序列后添加的!

 

理解两方面:【不要妄想一口吃个胖子,先1后2,不急着先2】

1、知道他说的是啥:理解他如何实现,能走通数据结构。【主要看图,走数据结构-步骤】

2、知道他为什么这么说:理解他设计的针对点,妙在哪里,怎么想出来的?

     *  怎么想出来的?

     *      (1)dump。是发现最后一个节点无法输出,再往下走一步就可以输出了!

     *      (2)逆序输出cur_lc到pre。【这个还不知道怎么想的?】

     *      (3)index。这个是发现应该大顺序,小逆序,如果不用index,就全是逆序,也不行。

 

步骤:

 

 

 

 

 

 

4、层序

(1)递归,层层输出

1、由于递归方法levelOrderRecursion_Height()只能输出第height层。所以需要在外层加个循环,来遍历所有的层。

2、递归方法levelOrderRecursion_Height():需要加个参数height,只能输出第height层。

3、height树高,需要调用height()方法求。height()方法递归实现。

 

(2)迭代=广度优先遍历

1、使用队列。

2、队列为空终止。

3、左右孩子入队列时记得判空,不空再进。

 

 

(3)迭代,层层输出

使用两个队列,交替执行【用李喜旺牛腩饭中的萝卜想通的】

 

具体实现:不按照“交替”,而是按照“临时-赋值”。

1、如果按照交替,两个while代码除了1和2交换,完全冗余。有划线,强迫症受不了。

2、按照临时-赋值,每次都是先把queue1一个个往外出,孩子全都临时存在queue2,最后queue1空之后,把queue2全都放回queue1,queue2清空。

 

 

特别:

1、广度优先遍历,在二叉树中,相当于层序。

2、深度优先遍历,在二叉树中,相当于先序吧??【我看是,还没分析】

【真的是啊!!!这个人就是用DFS实现的先序!】

 https://greyireland.gitbook.io/algorithm-pattern/shu-ju-jie-gou-pian/binary_tree#bfs-ceng-ci-ying-yong

 

 

 

 

二、代码

1、先序

/**
     * 先序,递归
     *
     */
    public static List<Integer> preOrderRecursion(List<Integer> list, BTNode root) {
        if (root == null) {
            return list;
        }

        list.add(root.val);

        list = preOrderRecursion(list, root.lc);
        list = preOrderRecursion(list, root.rc);
        return list;
    }

    /**
     * 先序,迭代
     *
     * 竟然用栈来实现,而且是先右后左!我惊呆了!!!
     * 我一直以为是队列。。。。
     *
     */
    public static List<Integer> preOrderIteration(List<Integer> list, BTNode root) {
        if (root == null) {
            return list;
        }
        Stack<BTNode> stack = new Stack<>();
        stack.push(root);

        while (!stack.isEmpty()) {
            BTNode curr = stack.pop();
            list.add(curr.val);
            if (curr.rc != null) {
                stack.push(curr.rc);
            }
            if (curr.lc != null) {
                stack.push(curr.lc);
            }
        }

        return list;
    }

    /**
     * 先序,Morris,也是迭代。
     *
     * 与中序唯一的不同之处,是在加索引前输出!!!
     */
    public static List<Integer> preOrderMorris(List<Integer> list, BTNode root) {
        BTNode cur = root;
        BTNode pre = null;
        while (cur != null) {
            if (cur.lc == null) {
                list.add(cur.val);
                cur = cur.rc;
            } else {
                pre = cur.lc;
                while (pre.rc != null && pre.rc != cur) {
                    pre = pre.rc;
                }

                if (pre.rc == null) {
                    list.add(cur.val); //与中序唯一的不同之处,是在加索引前输出!!!

                    pre.rc = cur;
                    cur = cur.lc;
                } else { //pre.rc == cur
                    pre.rc = null;
                    cur = cur.rc;
                }

            }
        }
        return list;
    }

 

2、中序

/**
     * 中序,递归
     *
     * @param list
     * @param root
     * @return
     */
    private static List<Integer> inOrderRecursion(List<Integer> list, BTNode root) {
        if (root == null) {
            return list;
        }

        inOrderRecursion(list, root.lc);
        list.add(root.val);
        inOrderRecursion(list, root.rc);

        return list;
    }

    /**
     * 中序,迭代
     *
     * 1、左链入栈:先把左子树全部入栈,直到最左边的叶子。
     * 2、栈不空时,弹出一个节点,输出val。
     * 3、无论栈空不空,每次都要对右孩子继续做“左链入栈”。
     * 4、循环退出条件,stack空且当前节点为空。
     *
     * @param list
     * @param root
     * @return
     */
    private static List<Integer> inOrderIteration(List<Integer> list, BTNode root) {
        Stack<BTNode> stack = new Stack();
        while (!stack.isEmpty() || root != null) {
            while (root != null) {
                stack.push(root);
                root = root.lc;
            }
            if (!stack.isEmpty()) {
                root = stack.pop();
                list.add(root.val);
            }
            //其实这个地方,有点像Morris的方法,只不过Morris用了叶子的指针存,而不是stack。
            // 一旦弹出一个节点,对右孩子继续做“左链入栈”
            root = root.rc;
        }
        return list;
    }

    /**
     * 中序,Morris
     * 其实也是迭代,不用栈的特殊迭代。
     *
     * 把二叉树拉直成链表。【也不是】
     * 把rc当成链表的next指针。
     *
     * 2、我的理解:Morris是用时间换空间。其实还不如用stack呢!【错了】
     *
     * 一直往左走,每次左不空就先标索引(左子树最右节点为pre),往左走。cur=cur.lc。
     * 直到左为空,就把cur输出,
     *      如果cur不是叶子,就是往右子树走。cur=cur.rc。
     *      如果cur是叶子,就是根据索引返回父节点。仍然是cur=cur.rc。
     *
     * 返回父节点后,再查一次pre,发现pre.rc=cur,则输出当前父亲(cur),并且把索引删掉。
     * 然后就遍历右子树啦。cur=cur.rc。
     *
     * 2个地方输出:输出后都是往右走,因为输出的是中。
     *      左空,输出cur,往右走/返回父亲,都是cur=cur.rc。
     *      左不空,且pre.rc==cur,输出cur,往右走cur=cur.rc。
     *
     * 往左走:
     *      只要左不空就标索引pre.rc=cur,标完索引往左走cur=cur.lc。
     *
     *
     * 3、空间和时间复杂度:
     * 空间复杂度为O(1):不用栈而是借助叶子的右指针不费空间,只用到了一个pre用O(1)。
     * 时间复杂度是O(n):
     * 每次查询所谓“前驱节点”其实就是找到左子树的最右节点,每个节点都要查两遍左子树,一遍是加索引,一遍是删索引。
     * 看似涉及树高度,是O(nlgn),实际上是每条边最多走两遍,是O(2n)也就是O(n)。
     *
     *
     * @param list
     * @param root
     * @return
     */
    private static List<Integer> inOrderMorris(List<Integer> list, BTNode root) {
        BTNode cur = root;
        BTNode pre = null;
        while (cur != null) {
            if (cur.lc == null) {
                list.add(cur.val);
                cur = cur.rc;
            } else {
                pre = cur.lc;
                while (pre.rc != null && pre.rc != cur) {//找到最右节点,即为pre。
                    pre = pre.rc;
                }
                if (pre.rc == null) {
                    pre.rc = cur;
                    cur = cur.lc;
                } else { // pre.rc == cur
                    pre.rc = null;
                    list.add(cur.val); //与先序唯一的不同之处,是在删索引的时候输出!!!
                    cur = cur.rc;
                }
            }
        }
        return list;
    }

 

 

3、后序

    /**
     * 后序,递归
     * @param list
     * @param root
     * @return
     */
    private static List<Integer> postOrderRecursion(List<Integer> list, BTNode root) {
        if (root == null) {
            return list;
        }
        postOrderRecursion(list, root.lc);
        postOrderRecursion(list, root.rc);
        list.add(root.val);
        return list;
    }

    /**
     * 后序,迭代
     *
     * 与先序迭代的区别:
     * 1、输出时,新的要插到前面。这是为了保证后序,最后输出根。
     * 2、左右子树入栈顺序,是先左后右。因为插到前面顺序正好与先序反着,先序是先右后左。
     *
     *
     * @param list
     * @param root
     * @return
     */
    private static List<Integer> postOrderIteration(List<Integer> list, BTNode root) {
        if (root == null) {
            return list;
        }

        Stack<BTNode> stack = new Stack<>();
        stack.push(root);

        while (!stack.isEmpty()) {
            BTNode cur = stack.pop();
            list.add(0, cur.val);
            if (cur.lc != null) {
                stack.push(cur.lc);
            }
            if (cur.rc != null) {
                stack.push(cur.rc);
            }
        }

        return list;
    }

    /**
     * 后序,Morris
     *
     * 先序DLR逆序输出?不行!问题在输出的是RLD,不是LRD!!
     *
     * 他人思路:【太妙了】
     * 1、dump.lc = root:
     *  cur从虚拟节点dump开始,而不是从root开始。。
     *  这是为了把最后一个节点输出,否则会剩下最右节点无法输出,绝妙。
     *  (不加dump最右节点无法输出,加上dump,dump无法输出,相当于往后赶了一步)
     *
     * 2、输出:大顺序小逆序,从cur_lc到pre的逆序
     *  在删除索引后输出从cur_lc到pre整个路径的逆序,其他地方均不输出。
     *  index保证,小序列内部逆序,但整体还是顺序往大序列后添加的!
     *
     *
     * 理解两方面:【不要妄想一口吃个胖子,先1后2,不急着先2】
     * 1、知道他说的是啥:理解他如何实现,能走通数据结构。【主要看图,走数据结构】
     * 2、知道他为什么这么说:理解他设计的针对点,妙在哪里,怎么想出来的?
     *  怎么想出来的?
     *      (1)dump。是发现最后一个节点无法输出,再往下走一步就可以输出了!
     *      (2)逆序输出cur_lc到pre。【这个还不知道怎么想的?】
     *      (3)index。这个是发现应该大顺序,小逆序,如果不用index,就全是逆序,也不行。
     *
     * @param list
     * @param root
     * @return
     */
    private static List<Integer> postOrderMorris(List<Integer> list, BTNode root) {
        BTNode dump = new BTNode();
        dump.lc = root;

        BTNode cur = dump;
        BTNode pre = null;

        while (cur != null) {
            if (cur.lc == null) {
                cur = cur.rc;
            } else {
                pre = cur.lc;
                while (pre.rc != null && pre.rc != cur) {
                    pre = pre.rc;
                }

                if (pre.rc == null) {
                    pre.rc = cur;
                    cur = cur.lc;
                } else {
                    int index = list.size(); //这个index可以保证,每次添加的小序列内部是逆序,但是大序列中还是往整个list后添加的!

                    BTNode cur_lc = cur.lc;

                    while (cur_lc != pre) {
                        list.add(index, cur_lc.val); //这个之前错弄成了cur.val
                        cur_lc = cur_lc.rc;
                    }
                    list.add(index, pre.val); //终止条件之后,再加进去一个pre。

                    pre.rc = null;
                    cur = cur.rc;
                }
            }
        }

        return list;
    }

 

4、层序

(1)层序,递归,按层数出。

    /**
     * 层序,递归,按照每一层输出
     *
     * 由于递归方法levelOrderRecursion_Height()只能输出第height层。
     * 所以需要在外层加个循环,来遍历所有的层。
     *
     * height树高,需要调用height()方法求。
     *
     * @param list
     * @param root
     * @return
     */
    private static List<Integer> levelOrderRecursion(List list, BTNode root) {
        int height = height(root);
        for (int i = 1; i <= height; i++) {
            List<Integer> tmp = new LinkedList();
            list.add(levelOrderRecursion_Height(tmp, root, i));
        }
        return list;
    }

    /**
     * 层序,递归【输出第height层】
     *
     * 思路:加个参数,树高。
     *
     * 这个只能打印第“height”层的节点。所以需要在外层加个循环,来遍历所有的层。
     *
     * @param list
     * @param root
     * @return
     */
    private static List<Integer> levelOrderRecursion_Height(List<Integer> list, BTNode root, int height) {
        if (height == 1) {
            if (root != null) {
                // System.out.println(root.val);
                list.add(root.val);
            } else {
                // System.out.println("空");
                list.add(null);
            }
            return list;
        }
        list = levelOrderRecursion_Height(list, root.lc, height - 1);
        list = levelOrderRecursion_Height(list, root.rc, height - 1);

        return list;
    }

    /**
     * 求树高,递归
     *
     * @param root
     * @return
     */
    private static int height(BTNode root) {
        if (root == null) {
            return 0;
        }
        return Math.max(height(root.lc), height(root.rc)) + 1;
    }

 

(2)层序=广度优先,迭代

    /**
     * 层序=广度优先,迭代
     * 使用队列
     *
     * 【应该没写完吧?】【OK了,之前出错是存入左右孩子时没有判空!现在没问题。2021-8-1 17:26:54】
     *
     * @param list
     * @param root
     * @return
     */
    public static List<Integer> levelOrderIteration(List<Integer> list, BTNode root) {
        BTNode curr = root;
        Queue<BTNode> queue = new LinkedList();
        queue.offer(curr);
        while (!queue.isEmpty()) {
            curr = queue.poll();
            list.add(curr.val);
            // System.out.print(curr.val + " ");

            if (curr.lc != null) {
                queue.offer(curr.lc);
            }
            if (curr.rc != null) {
                queue.offer(curr.rc);
            }
        }
        return list;
    }

 

(3)层序,迭代,按层输出

/**
     * 层序,迭代,按照每一层输出
     * 使用两个队列,交替执行【用李喜旺牛腩饭中的萝卜想通的】
     *
     * 具体实现:不按照“交替”,而是按照“临时-赋值”。
     *  1、如果按照交替,两个while代码除了1和2交换,完全冗余。有划线,强迫症受不了。
     *  2、按照临时-赋值,每次都是先把queue1一个个往外出,孩子全都临时存在queue2,最后queue1空之后,把queue2全都放回queue1,queue2清空。
     *
     * 完美! 2021-8-1 17:10:46
     *
     * @param list
     * @param root
     * @return
     */
    public static List<Integer> eachLevelOrderIteration(List list, BTNode root) {
        BTNode curr = root;
        Queue<BTNode> queue1 = new LinkedList();
        Queue<BTNode> queue2 = new LinkedList();
        queue1.offer(curr);

        while (!queue1.isEmpty() || !queue2.isEmpty()) {
            LinkedList<Integer> tmpList1 = new LinkedList<>();
            while (!queue1.isEmpty()) {
                curr = queue1.poll();
                // System.out.print(curr.val + ",");
                tmpList1.add(curr.val);
                if (curr.lc != null) {
                    queue2.offer(curr.lc);
                }
                if (curr.rc != null) {
                    queue2.offer(curr.rc);
                }
            }
            // System.out.println();
            list.add(tmpList1);
            queue1.addAll(queue2);
            queue2.clear();
        }
        return list;
    }

 

 

posted on 2021-08-02 00:58  西伯尔  阅读(391)  评论(0编辑  收藏  举报