Morris遍历: 二叉树之前的遍历方式有空间浪费的问题(递归实现也会占中栈空间)。Morris遍历时间复杂度O(N),额外空间复杂度O(1),通过利用原树中大量空闲指针的方式,达到节省空间的目的
1、Morris遍历概述
-
Morris遍历
- 二叉树之前的遍历方式有空间浪费的问题(递归实现也会占中栈空间)
- Morris遍历时间复杂度O(N),额外空间复杂度O(1),通过利用原树中大量空闲指针的方式,达到节省空间的目的
-
Morris遍历过程:
- 假设来到当前节点cur,开始时cur来到头节点位置
- 1)如果cur没有左孩子,cur向右移动(cur = cur.right)
- 2)如果cur有左孩子,找到左子树上最右的节点mostRight:
- 2.1)如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)
- 2.2)如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur = cur.right)
- 3)cur为空时遍历停止
-
Morris遍历的特点:
- 1)Morris遍历的过程中,cur指针会移动到每一个节点,也就是每个节点都会被cur访问到,但是有个规律:
- 如果一个节点有左孩子,这个节点会被cur访问2次,如何区分是第2次访问到:判断左树最右侧的节点是否指向自己,如果指向自己,说明是第2次访问到。
- 如果一个节点没有左孩子,这个节点会被cur访问1次,
- cur指针的指向顺序,就是Morris遍历的顺序。
- 2)Morris遍历的过程中,存在寻找mostRight的过程,所以每一个叶子节点都会被mostRight访问到,
- 作为父节点的左孩子的叶子节点,会在寻找父节点的左孩子的有边界的时候,被mostRight访问到,因为此时只有它一个节点,
- 对于作为父节点的右孩子的叶子节点,会在父节点的父节点在寻找父节点的有边界的时候被mostRight访问到,
- 要区分这个叶子节点是第一次被访问到还是第二次被访问到,同样是判断左树最右侧的节点是否指向自己,如果指向自己,说明是第2次访问到。
- 通常所说的被访问到是指cur访问到,而判断是第一次访问到还是第二次访问到,是根据mostRight的指向来判断的。
- 对于关注叶子节点的题目,我们要知道的是叶子节点实际上都是可以被mostRight访问到的
-
Morris遍历的应用:
- 基于基本的Morris遍历流程,通过在不同时机执行"访问"操作(如打印节点值),可以分别实现前序、中序和后序遍历。
- 1)前序遍历:cur第一次到达该节点时就访问,顺序为:根节点 -> 左子树 -> 右子树
- 2)中序遍历:cur第二次到达该节点时(即从左子树返回时)才访问,顺序为:左子树 -> 根节点 -> 右子树
- 3)后序遍历:cur第二次到达该节点时,逆序打印该节点左子树的右边界。最后,单独逆序打印整棵树的右边界
2、Morris遍历的实现
2.1、Morris遍历模板
- Morris遍历代码模板
- 思路:
-
根据Morris遍历的过程,写出的代码模板
-
1)如果cur没有左孩子,cur向右移动(cur = cur.right)
-
2)如果cur有左孩子,找到左子树上最右的节点mostRight:
-
2.1)如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)
-
2.2)如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur = cur.right)
-
3)cur为空时遍历停止
-
示例:
-
如下面这个二叉树:

-
Morris遍历的顺序为:a -> b -> d -> b -> e -> a -> c -> f -> c -> g
-
java
/**
* Morris遍历代码模板
* 思路:
* 根据Morris遍历的过程,写出的代码模板
* 1)如果cur没有左孩子,cur向右移动(cur = cur.right)
* 2)如果cur有左孩子,找到左子树上最右的节点mostRight:
* 2.1)如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)
* 2.2)如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur = cur.right)
* 3)cur为空时遍历停止
* 示例:
* 如下面这个二叉树:
* ............a
* ........../...\
* .........b.....c
* ......../.\.../.\
* .......d..e..f...g
* Morris遍历的顺序为:a -> b -> d -> b -> e -> a -> c -> f -> c -> g
*/
public static void morris(Node head) {
if (head == null) {
return;
}
// cur 指针,指向当前访问的节点,初始为头节点
Node cur = head;
// mostRight 指针,指向左子树最右侧的节点,初始为null
Node mostRight = null;
// 开始遍历,对应过程的:3)cur为空时遍历停止
while (cur != null) {
// 先将mostRight指针指向当前节点的左孩子,这样就可以根据mostRight是否为空,判断是否有左孩子
mostRight = cur.left;
if (mostRight != null) {
// 有左孩子,这个节点会被访问2次 ,
// 依据过程 2)如果cur有左孩子,找到左子树上最右的节点mostRight
// 找到左子树的最右侧节点,没有右子树或者上一次改成了指向自己,都要停止
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
// 第一次到达左子树的最右侧节点,对应过程2.1)
mostRight.right = cur;
cur = cur.left;
// 直接进行下一个 while循环
continue;
} else {
// 第二次到达左子树的最右侧节点,对应过程2.2)
mostRight.right = null;
}
}
// else{
// // 没有左孩子,这个节点会被访问1次,如果要单独挑出被访问一次的节点,可以加上else在这里判断
// }
// 对于过程 1)和2.2),即没有左孩子或者第二次访问节点,都会执行到这里,接下来访问当前节点的右孩子
cur = cur.right;
}
}
2.2、Morris遍历实现前序遍历
- Morris遍历实现前序遍历
- 思路:
- cur第一次到达该节点时就访问,顺序为:根节点 -> 左子树 -> 右子树
- 因为Morris遍历的过程中,有左子树的节点会被访问两次,没有的会被访问1次,
- 所以要挑出第一次访问的时候,就有两个地方:
- 1) 对于有左子树的节点,第一次访问是根据mostRight的right指针为空的时候,
- 2) 对于没有左子树的节点,第一次访问是根据cur的左孩子为空的时候,
java
/**
* Morris遍历实现前序遍历
* 思路:
* cur第一次到达该节点时就访问,顺序为:根节点 -> 左子树 -> 右子树
* 因为Morris遍历的过程中,有左子树的节点会被访问两次,没有的会被访问1次,
* 所以要挑出第一次访问的时候,就有两个地方:
* 1) 对于有左子树的节点,第一次访问是根据mostRight的right指针为空的时候,
* 2) 对于没有左子树的节点,第一次访问是根据cur的左孩子为空的时候,
*/
public static void morrisPre(Node head) {
if (head == null) {
return;
}
System.out.print("morrisPre: ");
Node cur = head;
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) {
// 第一次到达左子树的最右侧节点,对应过程2.1)
System.out.print(cur.value + " ");
mostRight.right = cur;
cur = cur.left;
// 直接进行下一个 while循环
continue;
} else {
// 第二次到达左子树的最右侧节点,对应过程2.2)
mostRight.right = null;
}
} else {
// 没有左子树,第一次访问是根据cur的左孩子为空的时候,
System.out.print(cur.value + " ");
}
cur = cur.right;
}
System.out.println();
}
2.3、Morris遍历实现中序遍历
- Morris遍历实现中序遍历
- 思路:
- cur第二次到达该节点时(即从左子树返回时)才访问,顺序为:左子树 -> 根节点 -> 右子树
- 因为Morris遍历的过程中,有左子树的节点会被访问两次,没有的会被访问1次,
- 对于只访问一次的节点,就是直接访问了,对于访问两次的节点,是要在第二次访问,
- 根据Morris的遍历代码可以看出,无论是没有左树还是第二次访问,都会走到最后指向右节点的位置,
- 所以我们只需要在指向右节点之前访问信息就可以了。
java
/**
* Morris遍历实现中序遍历
* 思路:
* cur第二次到达该节点时(即从左子树返回时)才访问,顺序为:左子树 -> 根节点 -> 右子树
* 因为Morris遍历的过程中,有左子树的节点会被访问两次,没有的会被访问1次,
* 对于只访问一次的节点,就是直接访问了,对于访问两次的节点,是要在第二次访问,
* 根据Morris的遍历代码可以看出,无论是没有左树还是第二次访问,都会走到最后指向右节点的位置,
* 所以我们只需要在指向右节点之前访问信息就可以了。
*/
public static void morrisIn(Node head) {
if (head == null) {
return;
}
System.out.print("morrisIn : ");
Node cur = head;
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) {
// 第一次到达左子树的最右侧节点,对应过程2.1)
mostRight.right = cur;
cur = cur.left;
// 直接进行下一个 while循环
continue;
} else {
// 第二次到达左子树的最右侧节点,对应过程2.2)
mostRight.right = null;
}
}
// 没有左孩子和第二次到达,都会走到这里
System.out.print(cur.value + " ");
cur = cur.right;
}
System.out.println();
}
2.4、Morris遍历实现后序遍历
- Morris遍历实现后序遍历
- 思路:
- cur第二次到达该节点时,逆序打印该节点左子树的右边界。最后,单独逆序打印整棵树的右边界,
- 根据Morris遍历的过程,第二次到达该节点的位置很容易找到,
- 麻烦的是如何逆序打印该节点左子树的右边界。
- 我们可以将左子树的有边界看成是一个链表,先用反转链表的方式将其转过来,然后打印,完成以后再反转回来。
java
/**
* Morris遍历实现后序遍历
* 思路:
* cur第二次到达该节点时,逆序打印该节点左子树的右边界。最后,单独逆序打印整棵树的右边界,
* 根据Morris遍历的过程,第二次到达该节点的位置很容易找到,
* 麻烦的是如何逆序打印该节点左子树的右边界。
* 我们可以将左子树的有边界看成是一个链表,先用反转链表的方式将其转过来,然后打印,完成以后再反转回来。
*/
public static void morrisPos(Node head) {
if (head == null) {
return;
}
System.out.print("morrisPos: ");
Node cur = head;
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) {
// 第一次到达左子树的最右侧节点,对应过程2.1)
mostRight.right = cur;
cur = cur.left;
// 直接进行下一个 while循环
continue;
} else {
// 第二次到达左子树的最右侧节点,对应过程2.2)
mostRight.right = null;
// 逆序打印左子树的有边界
printEdge(cur.left);
}
}
cur = cur.right;
}
// 单独打印整棵树的有边界
printEdge(head);
System.out.println();
}
/**
* 逆序打印右边界
*/
public static void printEdge(Node head) {
// 先逆序右边界组成的链
Node tail = reverseEdge(head);
// 此时打印,就是逆序的
Node cur = tail;
while (cur != null) {
System.out.print(cur.value + " ");
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;
}
2.5、Morris遍历判断是否为二叉搜索树
- Morris遍历判断是否为二叉搜索树
- 思路:
- 二叉搜索树的左右子树都要比其父节点大,
- 我们可以用中序遍历二叉树的方式,用一个变量记录上一个访问的节点的值,
- 如果当前节点的值小于等于上一个节点的值,说明不是二叉搜索树。
java
/**
* Morris遍历判断是否为二叉搜索树
* 思路:
* 二叉搜索树的左右子树都要比其父节点大,
* 我们可以用中序遍历二叉树的方式,用一个变量记录上一个访问的节点的值,
* 如果当前节点的值小于等于上一个节点的值,说明不是二叉搜索树。
*/
public static boolean isBST(Node head) {
if (head == null) {
return true;
}
Node cur = head;
Node mostRight = null;
// 前一个访问的节点的值
Integer pre = null;
boolean ans = true;
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;
}
}
if (pre != null && pre >= cur.value) {
ans = false;
}
// 中序遍历的方式,所以只需要在这里改pre的值即可
pre = cur.value;
cur = cur.right;
}
return ans;
}
整体代码和测试如下:
java
/**
* Morris遍历
* 二叉树之前的遍历方式有空间浪费的问题(递归实现也会占中栈空间)
* Morris遍历时间复杂度O(N),额外空间复杂度O(1),通过利用原树中大量空闲指针的方式,达到节省空间的目的
* <br>
* Morris遍历过程:
* 假设来到当前节点cur,开始时cur来到头节点位置
* 1)如果cur没有左孩子,cur向右移动(cur = cur.right)
* 2)如果cur有左孩子,找到左子树上最右的节点mostRight:
* 2.1)如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)
* 2.2)如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur = cur.right)
* 3)cur为空时遍历停止
* <br>
* Morris遍历的特点:
* 1)Morris遍历的过程中,cur指针会移动到每一个节点,也就是每个节点都会被cur访问到,但是有个规律:
* 如果一个节点有左孩子,这个节点会被cur访问2次,如何区分是第2次访问到:判断左树最右侧的节点是否指向自己,如果指向自己,说明是第2次访问到。
* 如果一个节点没有左孩子,这个节点会被cur访问1次,
* cur指针的指向顺序,就是Morris遍历的顺序。
* 2)Morris遍历的过程中,存在寻找mostRight的过程,所以每一个叶子节点都会被mostRight访问到,
* 作为父节点的左孩子的叶子节点,会在寻找父节点的左孩子的有边界的时候,被mostRight访问到,因为此时只有它一个节点,
* 对于作为父节点的右孩子的叶子节点,会在父节点的父节点在寻找父节点的有边界的时候被mostRight访问到,
* 要区分这个叶子节点是第一次被访问到还是第二次被访问到,同样是判断左树最右侧的节点是否指向自己,如果指向自己,说明是第2次访问到。
* 通常所说的被访问到是指cur访问到,而判断是第一次访问到还是第二次访问到,是根据mostRight的指向来判断的。
* 对于关注叶子节点的题目,我们要知道的是叶子节点实际上都是可以被mostRight访问到的
* <br>
* Morris遍历的应用:
* 基于基本的Morris遍历流程,通过在不同时机执行"访问"操作(如打印节点值),可以分别实现前序、中序和后序遍历。
* 1)前序遍历:cur第一次到达该节点时就访问,顺序为:根节点 -> 左子树 -> 右子树
* 2)中序遍历:cur第二次到达该节点时(即从左子树返回时)才访问,顺序为:左子树 -> 根节点 -> 右子树
* 3)后序遍历:cur第二次到达该节点时,逆序打印该节点左子树的右边界。最后,单独逆序打印整棵树的右边界
*/
public class MorrisTraversal {
/**
* Morris遍历代码模板
* 思路:
* 根据Morris遍历的过程,写出的代码模板
* 1)如果cur没有左孩子,cur向右移动(cur = cur.right)
* 2)如果cur有左孩子,找到左子树上最右的节点mostRight:
* 2.1)如果mostRight的右指针指向空,让其指向cur,然后cur向左移动(cur = cur.left)
* 2.2)如果mostRight的右指针指向cur,让其指向null,然后cur向右移动(cur = cur.right)
* 3)cur为空时遍历停止
* 示例:
* 如下面这个二叉树:
* ............a
* ........../...\
* .........b.....c
* ......../.\.../.\
* .......d..e..f...g
* Morris遍历的顺序为:a -> b -> d -> b -> e -> a -> c -> f -> c -> g
*/
public static void morris(Node head) {
if (head == null) {
return;
}
// cur 指针,指向当前访问的节点,初始为头节点
Node cur = head;
// mostRight 指针,指向左子树最右侧的节点,初始为null
Node mostRight = null;
// 开始遍历,对应过程的:3)cur为空时遍历停止
while (cur != null) {
// 先将mostRight指针指向当前节点的左孩子,这样就可以根据mostRight是否为空,判断是否有左孩子
mostRight = cur.left;
if (mostRight != null) {
// 有左孩子,这个节点会被访问2次 ,
// 依据过程 2)如果cur有左孩子,找到左子树上最右的节点mostRight
// 找到左子树的最右侧节点,没有右子树或者上一次改成了指向自己,都要停止
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
if (mostRight.right == null) {
// 第一次到达左子树的最右侧节点,对应过程2.1)
mostRight.right = cur;
cur = cur.left;
// 直接进行下一个 while循环
continue;
} else {
// 第二次到达左子树的最右侧节点,对应过程2.2)
mostRight.right = null;
}
}
// else{
// // 没有左孩子,这个节点会被访问1次,如果要单独挑出被访问一次的节点,可以加上else在这里判断
// }
// 对于过程 1)和2.2),即没有左孩子或者第二次访问节点,都会执行到这里,接下来访问当前节点的右孩子
cur = cur.right;
}
}
/**
* Morris遍历实现前序遍历
* 思路:
* cur第一次到达该节点时就访问,顺序为:根节点 -> 左子树 -> 右子树
* 因为Morris遍历的过程中,有左子树的节点会被访问两次,没有的会被访问1次,
* 所以要挑出第一次访问的时候,就有两个地方:
* 1) 对于有左子树的节点,第一次访问是根据mostRight的right指针为空的时候,
* 2) 对于没有左子树的节点,第一次访问是根据cur的左孩子为空的时候,
*/
public static void morrisPre(Node head) {
if (head == null) {
return;
}
System.out.print("morrisPre: ");
Node cur = head;
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) {
// 第一次到达左子树的最右侧节点,对应过程2.1)
System.out.print(cur.value + " ");
mostRight.right = cur;
cur = cur.left;
// 直接进行下一个 while循环
continue;
} else {
// 第二次到达左子树的最右侧节点,对应过程2.2)
mostRight.right = null;
}
} else {
// 没有左子树,第一次访问是根据cur的左孩子为空的时候,
System.out.print(cur.value + " ");
}
cur = cur.right;
}
System.out.println();
}
/**
* Morris遍历实现中序遍历
* 思路:
* cur第二次到达该节点时(即从左子树返回时)才访问,顺序为:左子树 -> 根节点 -> 右子树
* 因为Morris遍历的过程中,有左子树的节点会被访问两次,没有的会被访问1次,
* 对于只访问一次的节点,就是直接访问了,对于访问两次的节点,是要在第二次访问,
* 根据Morris的遍历代码可以看出,无论是没有左树还是第二次访问,都会走到最后指向右节点的位置,
* 所以我们只需要在指向右节点之前访问信息就可以了。
*/
public static void morrisIn(Node head) {
if (head == null) {
return;
}
System.out.print("morrisIn : ");
Node cur = head;
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) {
// 第一次到达左子树的最右侧节点,对应过程2.1)
mostRight.right = cur;
cur = cur.left;
// 直接进行下一个 while循环
continue;
} else {
// 第二次到达左子树的最右侧节点,对应过程2.2)
mostRight.right = null;
}
}
// 没有左孩子和第二次到达,都会走到这里
System.out.print(cur.value + " ");
cur = cur.right;
}
System.out.println();
}
/**
* Morris遍历实现后序遍历
* 思路:
* cur第二次到达该节点时,逆序打印该节点左子树的右边界。最后,单独逆序打印整棵树的右边界,
* 根据Morris遍历的过程,第二次到达该节点的位置很容易找到,
* 麻烦的是如何逆序打印该节点左子树的右边界。
* 我们可以将左子树的有边界看成是一个链表,先用反转链表的方式将其转过来,然后打印,完成以后再反转回来。
*/
public static void morrisPos(Node head) {
if (head == null) {
return;
}
System.out.print("morrisPos: ");
Node cur = head;
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) {
// 第一次到达左子树的最右侧节点,对应过程2.1)
mostRight.right = cur;
cur = cur.left;
// 直接进行下一个 while循环
continue;
} else {
// 第二次到达左子树的最右侧节点,对应过程2.2)
mostRight.right = null;
// 逆序打印左子树的有边界
printEdge(cur.left);
}
}
cur = cur.right;
}
// 单独打印整棵树的有边界
printEdge(head);
System.out.println();
}
/**
* 逆序打印右边界
*/
public static void printEdge(Node head) {
// 先逆序右边界组成的链
Node tail = reverseEdge(head);
// 此时打印,就是逆序的
Node cur = tail;
while (cur != null) {
System.out.print(cur.value + " ");
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遍历判断是否为二叉搜索树
* 思路:
* 二叉搜索树的左右子树都要比其父节点大,
* 我们可以用中序遍历二叉树的方式,用一个变量记录上一个访问的节点的值,
* 如果当前节点的值小于等于上一个节点的值,说明不是二叉搜索树。
*/
public static boolean isBST(Node head) {
if (head == null) {
return true;
}
Node cur = head;
Node mostRight = null;
// 前一个访问的节点的值
Integer pre = null;
boolean ans = true;
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;
}
}
if (pre != null && pre >= cur.value) {
ans = false;
}
// 中序遍历的方式,所以只需要在这里改pre的值即可
pre = cur.value;
cur = cur.right;
}
return ans;
}
/**
* 二叉树的节点定义
*/
public static class Node {
public int value;
Node left;
Node right;
public Node(int data) {
this.value = data;
}
}
public static void main(String[] args) {
Node head = new Node(4);
head.left = new Node(2);
head.right = new Node(6);
head.left.left = new Node(1);
head.left.right = new Node(3);
head.right.left = new Node(5);
head.right.right = new Node(7);
printTree(head);
morrisPre(head);
morrisIn(head);
morrisPos(head);
printTree(head);
System.out.println(isBST(head));
}
// for test -- print tree
public static void printTree(Node head) {
System.out.println("Binary Tree:");
printInOrder(head, 0, "H", 17);
System.out.println();
}
public static void printInOrder(Node head, int height, String to, int len) {
if (head == null) {
return;
}
printInOrder(head.right, height + 1, "v", len);
String val = to + head.value + to;
int lenM = val.length();
int lenL = (len - lenM) / 2;
int lenR = len - lenM - lenL;
val = getSpace(lenL) + val + getSpace(lenR);
System.out.println(getSpace(height * len) + val);
printInOrder(head.left, height + 1, "^", len);
}
public static String getSpace(int num) {
String space = " ";
StringBuffer buf = new StringBuffer("");
for (int i = 0; i < num; i++) {
buf.append(space);
}
return buf.toString();
}
}
3、题目:求二叉树的最小深度
- 题目:求二叉树的最小深度
- 给定一个二叉树,找出其最小深度。
- 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
- 说明:叶子节点是指没有子节点的节点。
- 测试链接 : https://leetcode.cn/problems/minimum-depth-of-binary-tree
3.1、二叉树的递归套路实现方法
- 二叉树的递归套路实现方法
- 思路:
- 根据二叉树的递归套路,我们需要最小的深度,那就要像左右节点要他们各自最小的深度,
- 要到以后,取他们最小的深度,再加上1,就是以x为头的树的最小深度
- 提交时方法名改为:minDepth
java
/**
* 二叉树的递归套路实现方法
* 思路:
* 根据二叉树的递归套路,我们需要最小的深度,那就要像左右节点要他们各自最小的深度,
* 要到以后,取他们最小的深度,再加上1,就是以x为头的树的最小深度
* 提交时方法名改为:minDepth
*/
public static int minDepth(TreeNode head) {
if (head == null) {
return 0;
}
return minDepthProcess(head);
}
/**
* 递归函数的定义:返回x为头的树,最小深度是多少
*/
public static int minDepthProcess(TreeNode x) {
if (x.left == null && x.right == null) {
// 叶节点,返回1
return 1;
}
// 左右子树起码有一个不为空
int leftH = Integer.MAX_VALUE;
if (x.left != null) {
leftH = minDepthProcess(x.left);
}
int rightH = Integer.MAX_VALUE;
if (x.right != null) {
rightH = minDepthProcess(x.right);
}
return 1 + Math.min(leftH, rightH);
}
3.2、Morris遍历实现方法
- Morris遍历实现方法
- 思路:
- 要判断一个数的深度,即是从根节点到叶子节点的距离,然后取一个最小值即可。
- 根据Morris遍历我们知道,叶子节点都会被mostRight指针遍历到,所以这个是可以通过mostRight指针来判断的。
- 我们只需要记录每一个节点的深度,就能统计出到叶子节点的距离。然后用一个变量取到他们中的最小值即可。
- 这里有两个问题:
- 1)根据Morris遍历,有些节点会被访问两次,而且是从叶子节点直接转到上层,深度变化如何处理?
- 对于这个问题,因为第一次到达叶子节点的时候,我们是用了mostRight来找子节点的最右节点,可以在这个过程中统计出子节点到最右节点的距离,
- 然后到第二次访问的时候,直接减去这个距离即可还原原来节点的深度。
- 2)根据Morris遍历,我们访问到最后一个节点的时候,就直接退出了,所以整棵树的最右边这条边是没有统计的。
- 所以我们在整体统计完成以后,需要单独统计一下整棵树的最右边的深度,需要的时候考虑上最小值即可。
- 提交时方法名改为:minDepth
java
/**
* Morris遍历实现方法
* 思路:
* 要判断一个数的深度,即是从根节点到叶子节点的距离,然后取一个最小值即可。
* 根据Morris遍历我们知道,叶子节点都会被mostRight指针遍历到,所以这个是可以通过mostRight指针来判断的。
* 我们只需要记录每一个节点的深度,就能统计出到叶子节点的距离。然后用一个变量取到他们中的最小值即可。
* 这里有两个问题:
* 1)根据Morris遍历,有些节点会被访问两次,而且是从叶子节点直接转到上层,深度变化如何处理?
* 对于这个问题,因为第一次到达叶子节点的时候,我们是用了mostRight来找子节点的最右节点,可以在这个过程中统计出子节点到最右节点的距离,
* 然后到第二次访问的时候,直接减去这个距离即可还原原来节点的深度。
* 2)根据Morris遍历,我们访问到最后一个节点的时候,就直接退出了,所以整棵树的最右边这条边是没有统计的。
* 所以我们在整体统计完成以后,需要单独统计一下整棵树的最右边的深度,需要的时候考虑上最小值即可。
* 提交时方法名改为:minDepth
*/
public static int minDepth(TreeNode head) {
if (head == null) {
return 0;
}
TreeNode cur = head;
TreeNode mostRight = null;
// 记录当前节点的深度的变量
int curLevel = 0;
// 记录最小高度的变量
int minHeight = Integer.MAX_VALUE;
while (cur != null) {
mostRight = cur.left;
if (mostRight != null) {
// 统计到左子树最右边节点的距离
int rightBoardSize = 1;
while (mostRight.right != null && mostRight.right != cur) {
rightBoardSize++;
mostRight = mostRight.right;
}
if (mostRight.right == null) {
// 第一次到达,深度加1
curLevel++;
mostRight.right = cur;
cur = cur.left;
continue;
} else {
// 第二次到达,此时的深度还没有减去右子树的深度,所以是子树最右侧节点的深度,需要统计到整体的里面
if (mostRight.left == null) {
minHeight = Math.min(minHeight, curLevel);
}
// 减去右子树的深度,还原到当前节点的深度
curLevel -= rightBoardSize;
mostRight.right = null;
}
} else {
// 只有一次到达的节点,直接统计深度,深度加1
curLevel++;
}
cur = cur.right;
}
// 单独统计整棵树的最右边的深度
int finalRight = 1;
cur = head;
while (cur.right != null) {
finalRight++;
cur = cur.right;
}
// 整个树的最右节点是一个叶子节点的时候,需要将最优节点的高度加进去,
// 如果不是叶子节点,说明有左节点,肯定比finalRight大。所以不能把finalRight加进去。
if (cur.left == null && cur.right == null) {
minHeight = Math.min(minHeight, finalRight);
}
return minHeight;
}
后记
个人学习总结笔记,不能保证非常详细,轻喷