二叉树遍历系列02-Morris遍历
一、引言
Morris遍历是一种时间复杂度为O(N),空间复杂度为O(1)的二叉树遍历方法,Morris遍历是一种迭代遍历,Morris遍历不需要借助额外的栈空间,只需要进行普通的循环迭代即可完成整棵二叉树的遍历,因为Morris通过利用结点空指针,完成了遍历的转移(十分巧妙的想法)
二、递归序
在二叉树遍历系列01,中讲解了二叉树遍历的递归序,这是一个非常重要的概念,因为这可以帮助我们更好的理解Morris遍历在遍历过程中对先序序列和中序序列以及后序序列的加工(在遍历的过程中何时处理结点)
三、Morris遍历算法简介
Morris的先序遍历、中序遍历以及后序遍历的算法框架基本上是一致的,区别在于对于每一个结点的处理时间,Morris遍历正是在不同时刻完成对结点的处理,从而加工出了,不同的遍历序列(先序、中序、后序)
3.1 Morris遍历过程简介
- 设置游标指针cur = root,如果cur == null,转2,否则转3
- 返回先序遍历结果
- 如果cur有左孩子,转4,否则转5
- 设置mostRight = cur.left,并且让mostRight一直往右走,即mostRight = mostRight.right,直到mostRight.right == null 或者mostRight.right == cur为止,转6
- cur = cur.right
- 如果cur的最右结点的右孩子为null,那么将mostRight.right = cur,cur = cur.left,否则转7
- most.right = null,转5
代码实现:
private void morris(TreeNode<V> root) {
TreeNode<V> cur = root;
while (cur != null) {
if (cur.left != null) {
TreeNode<V> mostRight = cur.left;
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遍历:
我们将cur指针指向的数字分别记录下来:
1、2、4、7、4、2、5、1、3、6
我们会发现Morris遍历的遍历序列中存在两种类型的结点
(1)在遍历序列中只出现了一次的结点,这种结点一定没有左孩子
(2)在遍历序列中出现了两次的结点,这种结点一定有左孩子
3.2 基于Morris遍历加工出先序序列
上述例子中的Morris序列为:
1、2、4、7、4、2、5、1、3、6
真实二叉树的先序序列为:
1、2、4、7、5、3、6
这两个序列的区别是:
出现了一次的结点,那么直接记录,出现了两次的结点只在第一次出现的时候记录,那么很明显我们需要在上述的代码框架中添加一些记录操作,用来记录我们需要的结点,那么我们怎么知道哪些结点是第一次出现,哪些结点会出现两次,并且怎么在第一次出现的时候记录呢?
现在来回答这个问题:
只出现一次的结点一定没有左孩子,根据这个特点,很容易知道我们应该在哪里记录,cur.left为null的时候,cur就应该被记录,具体位置参考下面代码:
根据Morris遍历的算法,我们可以知道,cur第一次到达一个结点的时候,如果cur有左孩子,那么算法就会去找该左孩子的最右结点,如果cur是第一次被访问,那么算法会将该最右结点的右孩子指针(此时为null),重定向到cur。当cur在继续遍历的过程中,通过它的right回到某个祖先结点时,那么这个祖先结点就是第二次被访问的时候,对应代码中的位置也如下,先序遍历,我们需要记录第一次到达的位置:
private void morris(TreeNode<V> root) {
TreeNode<V> cur = root;
while (cur != null) {
if (cur.left != null) {
TreeNode<V> mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
// 这个if分支是cur第一次被访问的时候,因为mostRight.right为null
// 如果cur是第二次被访问,那么cur一定是通过这个mostRight.right回来的
// mostRight.right不可能为null,只可能是指向cur
System.out.println(cur.val);
mostRight.right = cur;
cur = cur.left;
continue;
} else {
// 这里发现cur的most.right为cur,那么说明这是第二次回到cur
// 并且cur就是通过mostRight.right回来的,所以才会发现mostRight.right为cur
mostRight.right = null;
}
} else {
// 在原有代码的基础上添加这个else分支
// 这个else对应的就是cur.left == null的情况
// 对于先序序列,我们应该将其记录,这里以打印表示记录
System.out.println(cur.val);
}
cur = cur.right;
}
}
3.3 基于Morris遍历加工出中序序列
有了先序序列的加工经验,加工中序序列就显得非常容易了,还是先看Morris遍历的序列:
1、2、4、7、4、2、5、1、3、6
二叉树的中序序列:
7、4、2、5、1、3、6
这两个序列的区别是:
如果一个结点在Morris序列中只出现一次,那么中序序列将会记录,如果在Morris序列中出现两次,那么中序序列只会在该结点第二次出现时记录
有了先序序列的分析,我们已经清楚的掌握了,何时记录只出现一次的数字,何时在数字第一次出现时记录,何时在数字出现第二次时记录,具体看代码:
private void morris(TreeNode<V> root) { TreeNode<V> cur = root; while (cur != null) { if (cur.left != null) { TreeNode<V> mostRight = cur.left; while (mostRight.right != null && mostRight.right != cur) { mostRight = mostRight.right; } if (mostRight.right == null) { mostRight.right = cur; cur = cur.left; continue; } else { System.out.println(cur.val); mostRight.right = null; } } else { System.out.println(cur.val); } cur = cur.right; } }
3.4 基于Morris遍历加工出后序序列
在Morris序列中加工出后序序列比前两种都要麻烦,但是殊途同归,先看Morris序列:
1、2、4、7、4、2、5、1、3、6
再看后序序列:
7、4、5、2、6、3、1
直接观察比较困难,这里直接给出结论:
从Morris序列加工出后序序列只需要关注那些在Morris遍历中出现了两次的顶点,对于每一个出现了两次的顶点,在第二次访问它时,将它的左子树以right指针逆序输出(看一下code就明白了,比如第二次回到1的时候,将5、2加入后序序列),遍历结束后将这个操作对root结点也做一次
private void postOrderUseMorris(TreeNode<V> root, StringBuilder post) { TreeNode<V> cur = root; while (cur != null) { if (cur.left != null) { TreeNode<V> mostRight = cur.left; 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; // 第二次到达的时候,需要处理左孩子,逆序收集 TreeNode<V> reverse = reverseWithRight(cur.left); TreeNode<V> visit = reverse; while (visit != null) { post.append(visit.value).append("\t"); visit = visit.right; } reverseWithRight(reverse); } } cur = cur.right; } TreeNode<V> reverse = reverseWithRight(root); cur = reverse; while (cur != null) { post.append(cur.value).append("\t"); cur = cur.right; } reverseWithRight(reverse); } /** * 将root的以右孩子进行反转 * * @param node 结点 * @return 返回反转之后的结点 */ private TreeNode<V> reverseWithRight(TreeNode<V> node) { TreeNode<V> cur = node; TreeNode<V> next; TreeNode<V> pre = null; while (cur != null) { next = cur.right; cur.right = pre; pre = cur; cur = next; } return pre; }
四、Morris遍历代码完整实现
package binarytreevisit;
/**
* @author 西城风雨楼
*/
public class BinaryTree2<V> {
private TreeNode<V> root;
public String preOrderRecur() {
StringBuilder pre = new StringBuilder();
preOrder(root, pre);
return pre.toString();
}
public String inOrderRecur() {
StringBuilder in = new StringBuilder();
inOrder(root, in);
return in.toString();
}
public String postOrderRecur() {
StringBuilder post = new StringBuilder();
postOrder(root, post);
return post.toString();
}
private void preOrder(TreeNode<V> root, StringBuilder pre) {
if (root == null) {
return;
}
pre.append(root.value).append("\t");
preOrder(root.left, pre);
preOrder(root.right, pre);
}
private void inOrder(TreeNode<V> root, StringBuilder in) {
if (root == null) {
return;
}
inOrder(root.left, in);
in.append(root.value).append("\t");
inOrder(root.right, in);
}
private void postOrder(TreeNode<V> root, StringBuilder post) {
if (root == null) {
return;
}
postOrder(root.left, post);
postOrder(root.right, post);
post.append(root.value).append("\t");
}
public String inOrderMorris() {
StringBuilder in = new StringBuilder();
inOrderUseMorris(root, in);
return in.toString();
}
public String postOrderMorris() {
StringBuilder post = new StringBuilder();
postOrderUseMorris(root, post);
return post.toString();
}
public String preOrderMorris() {
StringBuilder pre = new StringBuilder();
preOrderUseMorris(root, pre);
return pre.toString();
}
private void inOrderUseMorris(TreeNode<V> root, StringBuilder in) {
TreeNode<V> cur = root;
while (cur != null) {
if (cur.left != null) {
TreeNode<V> mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
mostRight.right = cur;
cur = cur.left;
continue;
} else {
in.append(cur.value).append("\t");
mostRight.right = null;
}
} else {
in.append(cur.value).append("\t");
}
cur = cur.right;
}
}
private void preOrderUseMorris(TreeNode<V> root, StringBuilder pre) {
TreeNode<V> cur = root;
while (cur != null) {
if (cur.left != null) {
TreeNode<V> mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
pre.append(cur.value).append("\t");
mostRight.right = cur;
cur = cur.left;
continue;
} else {
mostRight.right = null;
}
} else {
pre.append(cur.value).append("\t");
}
cur = cur.right;
}
}
private void postOrderUseMorris(TreeNode<V> root, StringBuilder post) {
TreeNode<V> cur = root;
while (cur != null) {
if (cur.left != null) {
TreeNode<V> mostRight = cur.left;
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;
// 第二次到达的时候,需要处理左孩子,逆序收集
TreeNode<V> reverse = reverseWithRight(cur.left);
TreeNode<V> visit = reverse;
while (visit != null) {
post.append(visit.value).append("\t");
visit = visit.right;
}
reverseWithRight(reverse);
}
}
cur = cur.right;
}
TreeNode<V> reverse = reverseWithRight(root);
cur = reverse;
while (cur != null) {
post.append(cur.value).append("\t");
cur = cur.right;
}
reverseWithRight(reverse);
}
/**
* 将root的以右孩子进行反转
*
* @param node 结点
* @return 返回反转之后的结点
*/
private TreeNode<V> reverseWithRight(TreeNode<V> node) {
TreeNode<V> cur = node;
TreeNode<V> next;
TreeNode<V> pre = null;
while (cur != null) {
next = cur.right;
cur.right = pre;
pre = cur;
cur = next;
}
return pre;
}
/**
* 根据先序和中序重建二叉树
*
* @param pre 先序
* @param mid 中序
*/
public void rebuild(V[] pre, V[] mid) {
root = rebuildHelper(pre, 0, pre.length - 1,
mid, 0, mid.length - 1);
}
/**
* 根据先序和中序重建二叉树,函数不保证先序和中序序列的合法性
* 以及是否匹配
*
* @param pre 先序
* @param preLeft 先序起始位置
* @param preRight 先序终止位置
* @param mid 中序
* @param midLeft 中序起始位置
* @param midRight 中序终止位置
* @return 返回重建的二叉树的根节点
*/
private TreeNode<V> rebuildHelper(V[] pre,
int preLeft,
int preRight,
V[] mid,
int midLeft,
int midRight) {
if (preLeft > preRight) {
return null;
}
TreeNode<V> root = new TreeNode<>(pre[preLeft]);
if (preLeft == preRight) {
return root;
}
// 找到左右子树的分界位置
int leftBorder = Integer.MIN_VALUE;
for (int i = midLeft; i <= midRight; i++) {
if (root.value.equals(mid[i])) {
leftBorder = i;
break;
}
}
// 这里border可能找不到
if (leftBorder == Integer.MIN_VALUE) {
throw new RuntimeException("序列错误");
}
// 求出左子树的大小
int leftChildSize = leftBorder - midLeft;
// 递归创建左子树
root.left = rebuildHelper(pre, preLeft + 1, preLeft + leftChildSize,
mid, midLeft, leftBorder - 1);
// 递归创建右子树
root.right = rebuildHelper(pre, preLeft + 1 + leftChildSize, preRight,
mid, leftBorder + 1, midRight);
return root;
}
private static class TreeNode<V> {
public V value;
public TreeNode<V> left;
public TreeNode<V> right;
public TreeNode(V value, TreeNode<V> left, TreeNode<V> right) {
this.value = value;
this.left = left;
this.right = right;
}
public TreeNode(V value) {
this.value = value;
}
}
public static void main(String[] args) {
Integer[] pre = new Integer[]{1, 2, 4, 8, 9, 5, 3, 6, 10, 7};
Integer[] in = new Integer[]{8, 4, 9, 2, 5, 1, 10, 6, 3, 7};
BinaryTree2<Integer> tree = new BinaryTree2<>();
tree.rebuild(pre, in);
System.out.println("先序:");
System.out.println(tree.preOrderRecur());
System.out.println(tree.preOrderMorris());
System.out.println("中序:");
System.out.println(tree.inOrderRecur());
System.out.println(tree.inOrderMorris());
System.out.println("后序:");
System.out.println(tree.postOrderRecur());
System.out.println(tree.postOrderMorris());
}
}