面试中很值得聊的二叉树遍历方法——Morris遍历
Morris遍历
通过利用空闲指针的方式,来节省空间。时间复杂度O(N),额外空间复杂度O(1)。普通的非递归和递归方法的额外空间和树的高度有关,递归的过程涉及到系统压栈,非递归需要自己申请栈空间,都具有O(N)的额外空间复杂度。
Morris遍历的原则:
1. 假设当前节点为cur,
2. 如果cur没有左孩子,cur向右移动,cur = cur.right
3. 如果cur有左孩子,找到左子树上最右的节点mostRight
3.1 如果mostRight.right == null,令mostRight.right = cur,cur向左移动,cur = cur.left
3.2 如果mostRight.right == cur,令mostRight.right = null,cur向右移动,cur = cur.right
4. 如果cur == null 停止遍历
Morris:1242513637
1 public static void morris(TreeNode head){ 2 if(head == null) return; 3 TreeNode cur = head; 4 TreeNode mostRight = null; 5 while(cur != null){//cur为空遍历停止【4】 6 mostRight = cur.left;//是cur左子树上最右的节点 7 if(mostRight != null){//有左子树【3】 8 while(mostRight.right != null && mostRight != cur){//mostRight!=cur不加就会绕环循环 9 mostRight = mostRight.right;//找最右节点【3】 10 } 11 if(mostRight.right == null){//第一次来到cur【3.1】 12 mostRight.right = cur; 13 cur = cur.left; 14 continue;//执行循环 15 }else {//mostRight.right = cur第二次来到cur【3.2】 16 mostRight.right = null; 17 } 18 } 19 cur = cur.right;//没有左子树【2】 20 21 } 22 }
所有节点遍历左子树右边界的时间总代价O(N)
基于Morris的先中后序遍历
如果cur有左子树一定能遍历2次,没有左子树只能遍历一次。
先序遍历
Morris:1242513637
Morris先序:1245367
基于Morris的先序遍历,如果一个节点可以到达两次则打印第一次,如果只能到达一次直接打印。
1 public static void morrisPre(TreeNode head){ 2 if(head == null) return; 3 TreeNode cur = head; 4 TreeNode mostRight = null; 5 while(cur != null){//有左子树 6 mostRight = cur.left; 7 if(mostRight != null){ 8 while(mostRight.right != null && mostRight != cur){ 9 mostRight = mostRight.right; 10 } 11 if(mostRight.right == null){//第一次来到左子树 12 System.out.println(cur.val);//打印 13 mostRight.right = cur; 14 cur = cur.left; 15 continue; 16 }else { 17 mostRight.right = null; 18 } 19 }else{ 20 System.out.println(cur.val);//没有左子树 只会遍历一次 21 } 22 cur = cur.right; 23 } 24 }
中序遍历
Morris:1242513637
Morris中序:4251637
基于Morris的中序遍历,如果一个节点可以到达两次则打印第二次,如果只能到达一次直接打印。
1 public static void morrisIn(TreeNode head) { 2 if(head == null) return; 3 TreeNode cur = head; 4 TreeNode mostRight = null; 5 while(cur != null){ 6 mostRight = cur.left; 7 if(mostRight != null){ 8 while(mostRight.right != null && mostRight != cur){ 9 mostRight = mostRight.right; 10 } 11 if(mostRight.right == null){ 12 mostRight.right = cur; 13 cur = cur.left; 14 continue; 15 }else { 16 mostRight.right = null; 17 } 18 } 19 //没有左树跳过if直接打印,有左树第二次来到cur退出循环打印 20 System.out.println(cur.val); 21 cur = cur.right; 22 } 23 }
后序遍历
Morris:1242513637
Morris先序:4526731
基于Morris的后序遍历,如果一个节点可以到达两次则第二次到达时逆序打印左树的右边界,单独逆序打印整棵树的右边界。
(1)逆序右边界(等同于单链表的逆序)
1 public static TreeNode reverseEdge(TreeNode from) { 2 TreeNode pre = null; 3 TreeNode next = null; 4 while(from != null){ 5 next = from.right; 6 from.right = pre; 7 pre = from; 8 from = next; 9 } 10 return pre; 11 }
(2)逆序打印以head为头节点的右边界。
1 public static void printRightEdge(TreeNode head) { 2 TreeNode tail = reverseEdge(head);//逆转右边界 3 TreeNode cur = tail; 4 while(cur != null){ 5 System.out.println(cur.val + " "); 6 cur = cur.right; 7 } 8 reverseEdge(tail);//逆转回去 恢复原树 9 }
(3)在Morris遍历中按时机打印。
1 public static void morrisPost(TreeNode head){ 2 if(head == null) return; 3 TreeNode cur = head; 4 TreeNode mostRight = null; 5 while(cur != null){ 6 mostRight = cur.left; 7 if(mostRight != null){ 8 while(mostRight.right != null && mostRight != cur){ 9 mostRight = mostRight.right; 10 } 11 if(mostRight.right == null){ 12 mostRight.right = cur; 13 cur = cur.left; 14 continue; 15 }else {//第二次达到 逆序打印左子树的右边界 16 mostRight.right = null; 17 printRightEdge(cur.left); 18 } 19 } 20 cur = cur.right; 21 } 22 //最后退出循环之后,单独打印整棵树的右边界 23 printRightEdge(head); 24 }
Morris遍历的应用
如何判断一棵树是否是搜索二叉树?
中序遍历升序就是搜索二叉树。
1 public static boolean isBST(TreeNode head){ 2 if(head == null) return true; 3 TreeNode cur = head; 4 TreeNode mostRight = null; 5 int preValue = Integer.MIN_VALUE;//上一次得到的值 6 while(cur != null){ 7 mostRight = cur.left; 8 if(mostRight != null){ 9 while(mostRight.right != null && mostRight != cur){ 10 mostRight = mostRight.right; 11 } 12 if(mostRight.right == null){ 13 mostRight.right = cur; 14 cur = cur.left; 15 continue; 16 }else { 17 mostRight.right = null; 18 } 19 } 20 //中序遍历的操作时机在这里 所以在这里进行判断 21 if(cur.val <= preValue){//没有递增 22 return false; 23 } 24 preValue = cur.val; 25 cur = cur.right; 26 } 27 return true; 28 }
总结
树型DP问题的套路:定义一个类收集树的信息,定义一个递归函数,递归地收集左子树的信息和右子树的信息,再整合得到以当前节点为根的树的信息。
什么时候用树型DP什么时候用Morris遍历?
当必须得到左树的信息和右树的信息后,再在当前节点整合二者信息后做出判断则用树型DP是最优解。
当不需要整合左树和右树信息的时候,可以用树型DP,但是Morris是最优解。