Morris遍历:一种遍历二叉树的方式, 并且时间复杂度 O ( N ) O(N) O(N),额外空间复杂度 O ( 1 ) O(1) O(1),通过利用原树中大量空闲指针的方式,达到节省空间的目的
Morris遍历细节
假设来到当前节点cur
,开始时cur
来到头节点位置
- 如果
cur
没有左孩子,cur
向右移动 (cur = cur.right
) - 如果
cur
有左孩子,找到左子树上最右的节点mostRight
:- 如果
mostRight
的右指针指向空,让其指向cur
,然后cur
向左移动 (cur = cur.left
) - 如果
mostRight
的右指针指向cur
,让其指向null
,然后cur
向右移动 (cur = cur.right
)
- 如果
cur
为空时遍历停止
通常的二叉树遍历是通过栈来完成的,也就是栈记录了之前节点的位置,方便遍历完下面的节点后返回,这种方式是需要占用大量的系统空间的。
而看到Morris遍历过程可知,它是利用了底层树的最右子节点的右孩子指针指向当前节点的方式,记录当前节点的位置,方便在遍历完左子树后返回,然后再遍历右子树
遍历过程举例
- 图中橙色圈是
cur
当前指向的节点,橙色路径是指寻找左子树最右子节点的过程,蓝色圈是指找到的左子树最右子节点 - 整个遍历的结果是:
1、2、4、2、5、1、3、6、3、7
,可以看到有左孩子的节点都被遍历了两次 - 如何判断当前节点是第几次遍历?
- 获取到
cur
的左子树最右节点的右孩子如果是null
,则说明是当前节点cur
是第一次遍历 - 获取到
cur
的左子树最右节点的右孩子如果指向cur
,则说明是第二次遍历cur
- 注意:上图省略了二次查找的过程,如第二次回到节点
2
时,应当再次寻找节点2
的左子树最右节点,此时找到的最右节点的右孩子指向的正是节点2
,所以判定是第二次遍历节点2
,然后将右孩子指针重新指向null
- 注意:上图省略了二次查找的过程,如第二次回到节点
- 获取到
代码
public class MorrisTraversal {
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
value = data;
}
}
public static void morris(Node root) {
if (root == null) {
return;
}
Node cur = root;
Node mostRight = null;
// 过流程
while (cur != null) {
// mostRight是cur左孩子
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;
}
}
cur = cur.right;
}
}
}
复杂度估计
- 空间复杂度肯定是
O
(
1
)
O(1)
O(1),因为整个流程中只用到了
cur、mostRight
两个变量 - 时间复杂度为
O
(
N
)
O(N)
O(N),为什么?
-
由Morris遍历细节可知,
cur
每来到一个新的节点,都会调用寻找左子树最右子节点的过程,如下图:
-
而且每个过程都会调用两遍,因为之前说过
cur
有可能会有两次遍历。
比如:cur = 1
时,会遍历2、5、11
,遍历两遍cur = 2
时,会遍历4、9
,遍历两遍
-
所以说遍历右边界的总代价就是 O ( N ) O(N) O(N),整个遍历过程也就是 O ( N + N ) = O ( N ) O(N+N)=O(N) O(N+N)=O(N)
-
Morris先序遍历
- 如果当前节点没有左子树,则直接打印
- 如果当前节点有左子树,且是第一次来到该节点,则打印它;第二次来到不管它
public class MorrisTraversal {
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
value = data;
}
}
public static void preMorris(Node root) {
if (root == null) {
return;
}
Node cur = root;
Node mostRight = null;
// 过流程
while (cur != null) {
// mostRight是cur左孩子
mostRight = cur.left;
if (mostRight != null) {
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
System.out.print(cur.value + " "); // Processing
mostRight.right = cur;
cur = cur.left;
continue;
} else {
mostRight.right = null;
}
} else {
System.out.print(cur.value + " "); // Processing
}
cur = cur.right;
}
}
}
Morris中序遍历
- 如果当前节点没有左子树,则直接打印
- 如果当前节点有左子树,第一次来到该节点,不管它;第二次来到该节点,打印;
public class MorrisTraversal {
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
value = data;
}
}
public static void preMorris(Node root) {
if (root == null) {
return;
}
Node cur = root;
Node mostRight = null;
// 过流程
while (cur != null) {
// mostRight是cur左孩子
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;
}
}
System.out.print(cur.value + " "); // Processing
cur = cur.right;
}
}
}
Morris后序遍历
- 寻找左子树最右子节点的过程,将左子树的右边界记录下来
- 第二次来到该节点时,逆序打印左子树的右边界
- 最后单独逆序打印整棵树的右边界
public class MorrisTraversal {
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
value = data;
}
}
/**
* Morris 后序遍历
*/
public static void posMorris(Node root) {
if (root == null) {
return;
}
Node cur = root;
Node 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;
printRightEdge(cur);
}
}
cur = cur.right;
}
printRightEdge(root);
System.out.println();
}
/**
* 以x为头的树,逆序打印这棵树的右边界
*/
public static void printRightEdge(Node x) {
Node tail = reverseRightEdge(x);
Node cur = tail;
while (cur != null) {
System.out.println(cur.value + " ");
cur = cur.right;
}
reverseRightEdge(tail);
}
/**
* 右边界反转
*/
public static Node reverseRightEdge(Node from) {
Node pre = null;
Node next = null;
while (from != null) {
next = from.right;
from.right = pre;
pre = from;
from = next;
}
return pre;
}
}
题目应用
如何判断一棵树是搜索二叉树?
-
思路就是中序遍历整棵二叉树,然后判断遍历的结果是否是升序
-
利用Morris中序遍历,可以达到空间复杂度 O ( 1 ) O(1) O(1)
/**
* 判断一棵树是否是搜索二叉树
*/
public static boolean isBst(Node root) {
if (root == null) {
return false;
}
Node cur = root;
Node mostRight = null;
int preValue = Integer.MIN_VALUE;
// 过流程
while (cur != null) {
// mostRight是cur左孩子
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;
}
}
if (preValue >= cur.value) {
return false;
}
preValue = cur.value;
cur = cur.right;
}
return true;
}