Morris 算法
Morris 算法相较于其他二叉树遍历算法不同的是,可以使用非递归原地无影响地进行前中后序遍历。
这里推荐左神的视频,讲解详细
线索化
无论是哪种遍历顺序,线索化都是一样的。可以按照下图对照遍历规则结合看:

用 cur 节点遍历整棵树:
-
cur 没有左孩子,则 cur = cur.right;
-
cur 有左孩子,找到左孩子的最右子节点 mostRight,因为这是当前节点的左子树要遍历的最后一个元素
-
如果 mostRight 节点的右子树为空,则让该节点的右子树指向当前节点,用于回溯到 cur 节点,遍历右子树,而我们要知道 cur 是用来遍历所有节点的,所以不使用递归必然要涉及回溯操作,并且在回溯节点设置好后,要进行 cur 的遍历,也即
mostRight.right = cur; cur = cur.left; -
如果 mostRight 的右节点不为空,也就是说 mostRight 的右节点就是回溯点,同时需要撤销线索化。也即
cur = cur.right;mostRight.right = null;
-
大家可以对照一下,这是cur的遍历流程:
1、2、4、2、5、1、3、6
前提代码
public class Node {
public Node left;
public Node right;
public int val;
public Node(int val) {
this.val = val;
}
public Node(){}
public Node buildTree(int[] nums, int i){
if(nums.length==0)
return null;
if(i>=nums.length || nums[i] == 0)
return null;
Node root = new Node(nums[i]);
root.left = buildTree(nums,2*i+1);
root.right = buildTree(nums,2*i+2);
return root;
}
}
Morris 的前序遍历
聪明的读者肯定发现了,上面的遍历又会重复遍历的节点。更聪明的读者肯定又发现了,如果把所有的数字除掉第二遍,再看就是 1、2、4、5、3、6
这就是前序遍历的顺序,那么聪明的同学就发现了,其实这就和递归操作类似了,此时我们只要将第一次出现的结果进行打印就能得到前序遍历。
注意下面代码中的循环退出条件:
-
因为左孩子为空的时候不会去遍历他的左孩子
-
遍历右孩子时,如果也没有有孩子,那么会有线索,右孩子仍不为空
所以只有到了最后一个节点的下一个节点,才可能为空。这个可以自己拟图按照下面的代码走一遍,我说再多都不如你自己走一遍。
public List<Integer> preOrderTraverse(Node root) {
List<Integer> res = new ArrayList<>();
Node cur = root, mostRight;
while(cur != null) {
mostRight = cur.left;
if(mostRight != null) {
while(mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if(mostRight.right == null) {
res.add(cur.val);
mostRight.right = cur;
cur = cur.left;
} else {
cur = cur.right;
mostRight.right = null;
}
} else {
res.add(cur.val);
cur = cur.right;
}
}
return res;
}
Morris 的中序遍历
刚刚肯定有读者在想了,如果把重复遍历的节点的第一次省略会得到什么结果呢?很明显就是中序遍历。
前序和中序遍历其实都是一样的操作流程,只是输出位置不一样,这和递归的逻辑很像。
public List<Integer> preOrderTraverse(Node root) {
List<Integer> res = new ArrayList<>();
Node cur = root, mostRight;
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;
} else {
res.add(cur.val);
cur = cur.right;
mostRight.right = null;
}
} else {
res.add(cur.val);
cur = cur.right;
}
}
return res;
}
Morris 后序遍历
显然,应该有读者自己去试试了凑出后序遍历的规律,但发现好像并不是那么好凑。
从原理出发,后序遍历是:左右中
那么会发现一个现象,对于 b、c、d、e 这些节点,无论他们的左孩子有多少个节点,如何排列,当我开始打印节点 e 的时候,必然是要打印 e、d、c、b,也就是逆序输出,也就是链表倒转操作。而这个操作的时机,肯定是第二次遍历到 a 点的时候

再继续看这张图,了解更具体的原因:
假设 g 还有左孩子,那么就会继续往左孩子方向遍历;假设 g 没有左孩子只有右孩子 p,我们知道 p 会线索化到 g 的父节点,也就是说 p 的右节点会指向 g 的父节点, 此时根据左右中的顺序,就需要逆序遍历g、p。
如果到这还没有感觉,继续。
此时已经逆序遍历完了 g、p,我们知道 i 会线索化 h,所以第二次遍历 h 时,会逆序遍历 i;然后 h 时线索化到 b 的,所以……

大家应该明白大致的运行流程了。
private List<Integer> afterOrderTraverse(Node root) {
List<Integer> res = new ArrayList<>();
Node cur = root, mostRight;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) {
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == cur) {
mostRight.right = null;
printEdge(cur.left, res);
cur = cur.right;
} else {
mostRight.right = cur;
cur = cur.left;
}
} else {
cur = cur.right;
}
}
// 没有任何节点的左孩子为root,所以直接调用
printEdge(root, res);
return res;
}
private void printEdge(Node root, List<Integer> res) {
Node cur = reverseEdge(root);
Node temp = cur;
while (cur != null) {
res.add(cur.val);
cur = cur.right;
}
reverseEdge(temp);
}
private Node reverseEdge(Node root) {
Node cur = root;
Node pre = null;
Node after = root.right;
while(after != null) {
cur.right = pre;
pre = cur;
cur = after;
after = after.right;
}
cur.right = pre;
return cur;
}
有啥问题相互讨论哈
浙公网安备 33010602011771号