Morris遍历
概述
在二叉树遍历的深度优先遍历中,分析了如何用递归和非递归方式实现遍历二叉树,但是那些方法都无法做到空间复杂度为 O(1)
,对于递归方法,遍历时用到了函数栈,而对于非递归方法,则是直接申请了栈,这两种方法的空间复杂度均与树的高度相关,设树的高度为 h
,则空间复杂度为 O(h)
。事实上对于二叉树的遍历,还有一种空间复杂度为 O(1)
的方法,这就是——Morris遍历!!!
普通的递归和非递归解法在遍历过程中,在处理完某个节点后都是通过栈结构(函数栈或申请的栈)才可以返回上层去,正是二叉树的结构(只有从父节点指向孩子节点的指针,而没有从孩子节点指向父节点的指针)导致了从下层回到上层会如此困难。而Morris遍历的实质就是避免使用栈结构实现从下层返回上层,而是使下层到上层有指针,根据下层到上层的指针实现从下层到上层的移动。那么怎么才能有下层到上层的指针呢?
二叉树上的很多节点都有大量的空闲指针(如叶节点就有两个空闲指针),比如某些节点没有右孩子节点,那么这个节点的 right
指针就指向 null
,我们将之称为空闲状态,而Morris遍历就是使用了这些空闲指针。
Morris遍历
先不管先序遍历、中序遍历、后序遍历的相关概念,我们先看一下Morris遍历的过程:
设当前节点为 cur
,初始 cur = head
,则 cur
按照以下规则移动:
- 1.若
cur == null
,则过程停止,否则继续下面的过程; - 2.若
cur
无左子树,则令cur = cur.right
; - 3.若
cur
有左子树,则找到cur
左子树上最右的节点,记作mostRight
:- 1) 若
mostRight.right == null
,则令mostRight.right = cur
,即让mostRight
的right
指针指向当前节点cur
,然后令cur = cur.left
; - 2) 若
mostRight.right == cur
, 则令mostRight.right = null
,即让mostRight
的right
指针指向空,然后令cur = cur.right
。
- 1) 若
假设有如下的二叉树:
则根据上面的Morris遍历规则,其遍历如下:
(1)初始状态;
(2)cur
来到节点 4
,cur
此时有左子树,则找到其左子树的最右节点(即节点 3
),发现节点 3
的右指针指向 null
,那么让其指向 cur
,然后 cur = cur.left
;
(3)cur
来到节点 2
,cur
此时有左子树,则找到其左子树的最右节点(即节点 1
),发现节点 1
的右指针指向 null
,那么让其指向 cur
,然后 cur = cur.left
;
(4)cur
来到节点 1
,cur
此时没有左子树,则令 cur = cur.right
;
(5)cur
来到节点 2
,cur
此时有左子树,则找到其左子树的最右节点(即节点 1
),发现节点 1
的右指针指向 cur
,则令节点 1
的右指针指向 null
,然后令 cur = cur.right
;
(6)cur
来到节点 3
,cur
此时没有左子树,则令 cur = cur.right
;
(7)cur
来到节点 4
,cur
此时有左子树,则找到其左子树的最右节点(即节点 3
),发现节点 3
的右指针指向 cur
,则令节点 3
的右指针指向 null
,然后令 cur = cur.right
;
(8)cur
来到节点 6
,cur
此时有左子树,则找到其左子树的最右节点(即节点 5
),发现节点 5
的右指针指向 null
,那么让其指向 cur
,然后 cur = cur.left
;
(9)cur
来到节点 5
,cur
此时没有左子树,则令 cur = cur.right
;
(10)cur
来到节点 6
,cur
此时有左子树,则找到其左子树的最右节点(即节点 5
),发现节点 5
的右指针指向 cur
,则令节点 5
的右指针指向 null
,然后令 cur = cur.right
;
(11)cur
来到节点 7
,cur
此时没有左子树,则令 cur = cur.right
;
(12)cur == null
,则过程停止。
经过上述的Morris遍历,cur
一次到达的节点值为:4 → 2 → 1 → 2 → 3 → 4 → 6 → 5 → 6 → 7, 我们将这个序列称为Morris序,而且我们可以看出,在Morris遍历的过程中,对于有左子树的节点(4、2、6)都可以到达两次(图中到达两次的节点由粉色标出),而没有左子树的节点都只会到达一次(图中到达一次的节点由黄色标出)
而对于能到达两次的节点(图中粉色节点),我们可以用如下方法判断其是第一次到达,还是第二次到达:
- 如果其左子树的最右节点指向空即
mostRight.right = null
,则该节点为第一次到达; - 如果其左子树的最右节点指向其本身
mostRight.right = cur
,则该节点为第二次到达。
Morris遍历的代码如下:
public void morris(TreeNode head) {
if (head == null) {
return;
}
TreeNode cur = head;
TreeNode mostRight = null;
while (cur != null) { // 过程出口
mostRight = cur.left;
if (mostRight != null) { // cur有左子树
// 找到cur左子树的最右节点
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
// 如果mostRight.right == null,则让其指向cur
if (mostRight.right == null) {
mostRight.right = cur;
cur = cur.left; // cur左移
continue; // 回到外层while循环
} else { // mostRight == cur,则让其指向null
mostRight.right = null;
}
}
// cur没有左子树或其左子树最右节点指向cur,均要将cur右移
cur = cur.right;
}
}
对于Morris遍历,其时间复杂度为O(N),空间复杂度为O(1),其中N为二叉树节点个数。
Morris遍历实现深度优先遍历
前序遍历
对于Morris遍历实现前序遍历:
- 对于
cur
只能到达一次的节点(无左子树的节点),cur
到达时直接打印; - 对于
cur
可以到达两次的节点(有左子树的节点),cur
到达第一次时打印,第二次到达时不打印。
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
return morrisMethod(root);
}
/**
* Morris遍历
* @param root
* @return
*/
private List<Integer> morrisMethod(TreeNode root) {
List<Integer> list = new ArrayList<>();
if (root == null) {
return list;
}
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;
list.add(cur.val); // 第一次到的时候打印
cur = cur.left;
continue;
} else {
mostRight.right = null; // 第二次到的时候不打印
}
} else { // 到达一次的节点
list.add(cur.val);
}
cur = cur.right;
}
return list;
}
}
中序遍历
对于Morris遍历实现中序遍历:
- 对于
cur
只能到达一次的节点(无左子树的节点),cur
到达时直接打印; - 对于
cur
可以到达两次的节点(有左子树的节点),cur
到达第一次时不打印,第二次到达时打印。
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
return morrisMethod(root);
}
/**
* Morris遍历
* @param root
* @return
*/
private List<Integer> morrisMethod(TreeNode root) {
List<Integer> list = new ArrayList<>();
if (root == null) {
return list;
}
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 {
list.add(cur.val); // 第二次到的时候打印
mostRight.right = null;
}
} else { // 到达一次的节点
list.add(cur.val);
}
cur = cur.right;
}
return list;
}
}