经典教程:二叉树前、中、后遍历详解【递归+迭代+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
(3)Morris前序遍历【迭代】
中序改成前序。与中序唯一的不同之处,是在加索引前输出。
【注意】
Morris学习顺序:中序-前序-后序进栈顺序右,左。出栈左右。
Morris思想:看中序。
2、中序
(1)递归法
LDR,中间输出
(2)迭代法
1、左链入栈:先把左子树全部入栈,直到最左边的叶子。
2、栈不空时,弹出一个节点,输出val。
3、无论栈空不空,每次都要对右孩子继续做“左链入栈”。
4、循环退出条件,stack空且当前节点为空。
常见的错误版本:【我最开始看的教程】
(1)https://blog.csdn.net/yangfeisc/article/details/44497429
错误位置:【绿框】
改错:不应该是直接把if去掉,而是把tree=tree.right拿到if外面!也就是说,无论栈是否空,都要往右走。
(3)Morris中序遍历【原版】
我的理解:
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、左右子树入栈顺序,是先左后右。因为插到前面顺序正好与先序反着,先序是先右后左。
(3)Morris后序遍历
与中序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实现的先序!】
二、代码
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; }
作者:西伯尔
出处:http://www.cnblogs.com/sybil-hxl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。