morris算法(莫里斯算法)
Morris算法, 我们最常用于解决二叉树的遍历问题, 以及与遍历相关的一些问题(其实在二叉树系列问题中, 我们解决各种问题的时候都要涉及到一个对二叉树的遍历)
我们先来介绍几个常见的简单的二叉树遍历方式:
常见的二叉树遍历方式: ①: 递归 ②: 迭代
递归方式:
使用递归解决二叉树的遍历本质其实很简单, 我们只需要编写一个递归方法, 在其中先进行向左遍历, 遍历到最后之后开始向右遍历, 这样我们对于我们递归到的结点就会有一个打印时机的区分:(如下: )
- 在向左遍历之前打印:
- 就是第一次刚刚到这个节点的时候打印, 这个时候肯定就是先序遍历
- 在向左遍历之后, 向右遍历之前打印:
- 这个时候就是我们第一次遍历节点的时候并没有打印, 而是第一次回溯的时候进行了一个打印, 打印之后才去遍历右边, 所有这就是中序遍历
- 在向左和向右都遍历完之后打印:
- 这个时候就是我们第一次遍历节点的时候没有打印(向左递归过程中), 第一次回溯的时候也没有打印, 第二次回溯的时候才进行了打印, 这个时候就是后序遍历
一个节点(包括叶子结点), 肯定是会经过三次, 第一次就是向左遍历或者向右遍历的过程中, 第二次和第三次就是向左和向右遍历完成之后回溯的时候各会遍历到一次, 而先序遍历就是第一次遍历到结点的时候打印, 中序遍历就是第二次遍历到结点的时候打印, 后序遍历就是第三次遍历到结点的时候打印
递归方式中序遍历代码举例:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> resultList = new ArrayList<>();
priIT(root, resultList);
return resultList;
}
private void priIT(TreeNode node, List<Integer> resultList) {
//递归终止条件
if (node == null) {
return;
}
//递归操作
priIT(node.left, resultList);
resultList.add(node.val);
priIT(node.right, resultList);
}
迭代方式:
使用迭代的方式完成二叉树的遍历其实是和使用递归的方式完成二叉树的遍历一样的, 在递归的方式遍历过程中我们隐式的维护了一个栈结构, 而我们如果是使用迭代的方式完成二叉树的遍历, 其实大多数的时候都是会选择显式的维护一个栈结构, 然后来完成对二叉树的遍历操作
迭代方式中序遍历代码举例:
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> resultList = new ArrayList<>();
//使用双端队列来模拟栈
Deque<TreeNode> stack = new LinkedList<>();
//一直遍历到最左下角
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
//出栈之后就相当于左子节点和当前节点都处理完了, 接下来就要对右子节点进行一个同样的操作
root = stack.pop();
resultList.add(root.val);
root = root.right;
}
return resultList;
}
我们为什么要使用莫里斯算法?(莫里斯算法的好处是什么?)
莫里斯算法和线索化二叉树的思想是一样的, 都是利用了二叉树空余的指针来存储引用, 进而减少了空间复杂度, 使用递归和迭代完成二叉树遍历的时候时间复杂度和空间复杂度都是O(n), 而使用morris算法实现二叉树的遍历的时间复杂度为O(n), 但是空间复杂度则只有O(1)
- 注意: 这里我们说的遍历二叉树指的是遍历一般的二叉树, 如果是遍历线索化二叉树则除外, 线索化二叉树遍历的空间复杂度本来就是O(1)
莫里斯算法实现思想:
当前节点cur, 一开始的cur来到整棵树的头结点位置:
-
cur无左树, cur = cur.right
-
cur有左树, 找到左树最右节点mostright
1.1. mostright的右指针指向null的时候, mostright.right = cur, cur = cur.left
1.2. mostright的右指针指向cur的时候, mostright.right = null, cur = cur.right
上面第一个大条件, 就是判断当前节点有无左树, 如果没有左树, 那么我们直接就让当前引用指向当前节点的右子节点即可, 都没有左树了, 这个时候我们肯定是指针右移
上面的第二个大条件, 就是判断如果有左树, 那么我们就要找到左树上最优的结点, 并且分为以下两种情况:
- 如果mostright的右指针指向了null的时候, 我们就让mostright的右指针指向cur, 之后我们就需要让引用cur指向当前节点的左子节点(注意: 这个时候就是我们第一次遍历到这个节点)
- 如果mostright的右指针已经指向了cur了, 这个时候我们就让mostright的右指针重新指向null, 之后我们就需要让引用cur指向当前节点的右子节点(这个时候这个cur就是第二次指向同一个结点了)
莫里斯算法会经过所有结点, 但是对于没有左子节点的结点只会到达一次, 而对于有左子节点的结点会到达两次, 通过以上思想就可以实现莫里斯算法来遍历二叉树
- 只要是cur指针能指向的结点都是可以遍历到的结点, cur指针几次指向该结点, 我们就称之为到达了几次该结点
莫里斯算法也是先走左边, 只有左边没有的时候或者是左边走完的时候我们才会去走右边(也就是cur右移)
- 左边走完其实也就是我们左子树的真实最右子节点的右指针已经被二次修改过的时候(也就是重新置空的时候), 这个时候就算是右边走完了
使用莫里斯算法实现二叉树的中序遍历:
public static void morrisIn(Node head) {
//如果头结点为空, 直接返回
if(head == null){
return;
}
//开始的时候让cur指针指向头结点
Node cur = head;
//开始的时候mostright指向null
Node mostRight = null;
//如果当前节点不为null, 那么就循环
while (cur != null){
//让mostright指向cur的左子树的根节点(也就是左子节点), 为了找左子树的最右结点
mostRight = cur.left;
//如果cur的左子树不为空
if(mostRight != null){
//如果mostright的右子树不为空, 并且右子节点不为cur, 那么循环, 一直找到最右子节点
while (mostRight.right !=null && mostRight.right != cur){
//mostright一直右移, 最终就会找到最右子节点
mostRight = mostRight.right;
}
//从上面while循环出来之后mostright已经是当前节点的左子树上的最右子节点了
//这个时候分了两种情况:
/*
情况一: mostright的右子节点为null的时候, 这个时候我们就要让mostright的右子节点指向当前节点cur, 然后让cur指向当前节点的左子节点, 然后停止本次循环即可
*/
if(mostRight.right == null){
mostRight.right = cur;
cur = cur.left;
continue;
/*
情况二 mostright的右子节点为cur的时候, 这个时候我们让mostright的右子节点重新指向null, 然后让cur指向当前节点的右子节点, 当前节点的左边就算是执行完了
*/
}else {
//注意: 这个时候我们让cur指向当前节点的右子节点是在外面和cur左子树为空情况合并到了一起执行的
mostRight.right = null;
}
}
//如果cur的左子树为空, 那么直接让cur指向当前节点的右子节点即可
System.out.print(cur.value+" ");
cur = cur.right;
}
System.out.println();
}