Morris遍历是一种遍历二叉树的方式,时间复杂度O(N),空间复杂度O(1)
之前的遍历方法,不管是递归(系统帮我们压栈,空间消耗是树的高度)或自己创建栈,都需要额外的空间
思想:利用树中空闲指针,来节省空间
面试里可以聊聊,笔试就别写了,还是有点复杂的
遍历细节:
来到了cur节点,初始位置是根节点
1、cur无左孩子,cur=cur.right
2、cur有左孩子,找到左子树的最右节点mostRight
1)若mostRight右指针为null,则mostRight.right=cur,然后cur=cur.left
注:这一步实际上记录了回溯到子树根节点的路径
2)若mostRight右指针为cur,则mostRight.right=null,然后cur=cur.right
注:这一步实际上删除了前面记录的路径,且这棵子树已经遍历完成了
3、cur为null时,遍历结束
规律:一个节点有左树,则它一定会被遍历到2次,没有就只会遍历1次,可以根据这个节点左子树的最右节点指向谁,看是第几次遍历到它
在最简单递归遍历二叉树中,每个节点会被遍历三次:
public static void process(Node node) {
if (node == null) {
return;
}
// 1
process(node.left);
// 2
process(node.right);
// 3
}
但在morris遍历中,只有2次或1次
代码:
public static void morris(Node head) {
if (head == null) return;
Node cur = head;
Node mostRight = null;
while (cur != null) {
mostRight = cur.left; // mostRight是cur的左孩子
if (mostRight != null) {
// 有左子树
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
} // 找到了左子树的mostRight
if (mostRight.right == null) {
// 第一次来到cur
mostRight.right = cur;
cur = cur.left;
continue;
} else {
// mostRight.right==cur
// 这是第二次来到cur
mostRight.right = null;
}
}
cur = cur.right;
}
}
时间复杂度分析:
发现对于每个结点,需要遍历它左子树的右边界,且所有节点这样的遍历都是不重复的,所以总时间代价是O(N)
改造
morris遍历序为:1242513637
改造为先序遍历:
1、只遍历一次的节点,这一次的时候输出(或别的操作)
2、遍历两次的节点,在第一次遍历时输出
代码:
public static void morris(Node head) {
if (head == null) return;
Node cur = head;
Node mostRight = null;
while (cur != null) {
mostRight = cur.left; // mostRight是cur的左孩子
if (mostRight != null) {
// 有左子树
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
} // 找到了左子树的mostRight
if (mostRight.right == null) {
// 第一次来到cur
System.out.println(cur.val);
mostRight.right = cur;
cur = cur.left;
continue;
} else {
// mostRight.right==cur
// 这是第二次来到cur
// 不打印
mostRight.right = null;
}
} else {
// 没有左树
System.out.println(cur.val);
}
cur = cur.right;
}
}
改造为中序遍历:
1、只遍历一次的节点,这一次的时候输出(或别的操作)
2、遍历两次的节点,在第2次遍历时输出
public static void morris(Node head) {
if (head == null) return;
Node cur = head;
Node mostRight = null;
while (cur != null) {
mostRight = cur.left; // mostRight是cur的左孩子
if (mostRight != null) {
// 有左子树
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
} // 找到了左子树的mostRight
if (mostRight.right == null) {
// 第一次来到cur
mostRight.right = cur;
cur = cur.left;
continue;
} else {
// mostRight.right==cur
// 这是第二次来到cur
mostRight.right = null;
}
}
System.out.println(cur.val); // 只加了这一行
cur = cur.right;
}
}
改造为后序遍历:
1、不管只遍历1次的节点,对于能够遍历2次的节点,第二次遍历时,逆序打印它左子树的右边界
2、来到最后一个节点后,逆序打印整棵树的右边界
逆序打印怎么实现?
先把每个结点的右指针指向父节点,打印后,再把指针还原回来
public static void morrisPos(Node head) {
if (head == null) return;
Node cur = head;
Node mostRight = null;
while (cur != null) {
mostRight = cur.left; // mostRight是cur的左孩子
if (mostRight != null) {
// 有左子树
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
} // 找到了左子树的mostRight
if (mostRight.right == null) {
// 第一次来到cur
mostRight.right = cur;
cur = cur.left;
continue;
} else {
// mostRight.right==cur
// 这是第二次来到cur
mostRight.right = null;
printEdge(cur.left);
}
}
cur = cur.right;
}
printEdge(head);
System.out.println();
}
// 逆序打印以x为root的树的右边界
public static void printEdge(Node x) {
Node tail = reverseEdge(x);
Node cur = tail;
while (cur != null) {
System.out.println(cur.val + " ");
cur = cur.right;
}
reverseEdge(tail);
}
public static Node reverseEdge(Node from) {
Node pre = null;
Node next = null;
while (from != null) {
next = from.right;
from.right = pre;
pre = from;
from = next;
}
return pre;
}
morris遍历的应用
判断搜索二叉树
将中序遍历改造,维护一个变量preValue,在遍历过程中,当发现preValue有下降,就返回false,若遍历完毕都没有下降,就返回true
public static boolean isBST(Node head) {
if (head == null) return true;
Node cur = head;
Node mostRight = null;
int preValue = Integer.MIN_VALUE;
while (cur != null) {
mostRight = cur.left; // mostRight是cur的左孩子
if (mostRight != null) {
// 有左子树
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
} // 找到了左子树的mostRight
if (mostRight.right == null) {
// 第一次来到cur
mostRight.right = cur;
cur = cur.left;
continue;
} else {
// mostRight.right==cur
// 这是第二次来到cur
mostRight.right = null;
}
}
if (cur.val <= preValue) {
return false;
}
preValue = cur.val;
cur = cur.right;
}
return true;
}
判断BST问题实际上是一个遍历问题,morris遍历省掉了额外的栈空间
总结:
当问题要求我们获得某节点左右子树的完整信息,就使用普通的递归遍历方法,否则就使用morris遍历,效率最高