一篇文章看完怎么遍历二叉树(递归,迭代,Morris)!
作者:互联网
二叉树数据结构
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
}
理解递归序
如何用递归遍历一颗二叉树
public void recursion(TreeNode root) {
if (root != null) {
recursion(root.left);
recursion(root.right);
}
}
上图为执行代码后的走向,我们从根节点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);
//遍历完右子树后第三次进入
}
}
迭代方法实现遍历
递归的写法十分简单,因为递归相当于隐式维护了一个栈,而迭代则需要我们显式的维护一个栈,那么对于前中后序遍历,我们显然需要制定不同的规则来实现
前序遍历
规则如下:
- 弹出就打印
- 有右压右
- 有左压左
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无法继续时,弹出节点就打印,然后去弹出节点的右树继续执行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;
}
}
}
}
总体思路就是用一个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就是最右节点,如下图所示
图中节点1的左子树最右边界为5,这个很好理解
然后Morris遍历需要遵循以下两条规则:
- 当cur的左孩子为null时,
cur = cur.right
- 当cur的左孩子不为null时,如果我们是第一次进入cur指针指向的节点,那么先找到mostRight,将mostRight的右孩子指向cur,
cur = cur.left
, 如果是第二次进入cur指针指向的节点,那么先找到mostRight,将mostRight的右孩子还原为null,cur = cur.right
第一次看不懂不要紧,先跟着图过一遍,如下图所示
整理以下得到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序输出一颗二叉树的先序,中序,后序序列呢
每当第一次访问节点时就打印,打印出来的就是先序序列
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;
}
}
}
那么后序怎么实现呢?
后序的改编与先序与中序略有不同
我们需要在第二次访问无左子树节点时,倒着打印他左子树的右边界,在整棵树遍历完之后,倒着打印整棵树的右边界,
如图所示,第二次访问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