二叉树的深度优先遍历之先序中序后序递归非递归Morris遍历全解
递归版本
二叉树的深度优先遍历,包括:
1.前序遍历
2.中序遍历
3.后序遍历
他们是如何定义的呢?
见代码:
public void traverse(TreeNode root) {
if (root == null) {
return;
}
// 先序,preOrder,第一次到达该节点处
res.get(0).add(root.val);
traverse(root.left);
// 中序,inOrder第二次到达该节点处
res.get(1).add(root.val);
traverse(root.right);
// 后序,postOrder第三次到达该节点处
res.get(2).add(root.val);
}
如代码所示:
- 先序遍历是刚走到该节点便获取该节点的值,然后递归遍历左子树、右子树(
中左右
); - 中序遍历是递归遍历完左子树,然后获取该节点的值,最后再递归遍历右子树(
左中右
); - 后序遍历是递归遍历完左子树,再递归遍历右子树,最后获取该节点的值(
左右中
);
注:上述代码中res的声明为:List<List<Integer>> res
;
如图所示:
图中描述了递归在树的各个节点之间的抽象过程:
图形说明:
- 箭头指向表示递归函数访问到该节点;
- N表示null节点,指向null节点的指针,表示递归函数进入到null节点的判断处。
结合递归函数看该图就很简单了:
- 先序遍历,就是第一个箭头指向时打印的地方(中左右,从上往下,刚进入到该节点就获取该节点的值)
- 中序遍历,就是第二个箭头指向时打印的地方(左中右,遍历左边节点返回,然后再获取该节点的值,然后访问右节点)
- 后序遍历,就是第三个箭头指向时打印的地方(左右中,遍历完成左、右两边节点后返回,最后再获取该节点的值)
非递归版本遍历的统一模板
前边图形描述了递归版本的过程,非递归版本即模拟该过程即可:
详见代码:
public void nonRecursiveAllInOne(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
// 后序遍历,上一次访问的节点
TreeNode pre = null;
Deque<TreeNode> stack = new ArrayDeque<>();
while (!stack.isEmpty() || cur != null) {
// 一直往左边走
while (cur != null) {
// 先序遍历,刚访问该节点就获取该节点的值
res.get(0).add(cur.val);
stack.push(cur);
cur = cur.left;
}
// 一直往左边走后,cur最后会为空
// 此时获取压入栈中的元素(没弹出)
cur = stack.peek();
// 如果当前节点的右节点是前一次访问的节点,那么是第三次回来了,就是后序遍历
// 如果当前节点的右节点是空,因为我们代码一直往左边走,所以右边的null节点是没法访问到的(而且右边的null节点也没必要访问),此时,中序遍历和后序遍历都在此处了
if (cur.right == pre || cur.right == null) {
if (cur.right == null) {
// 如果cur.right是null,那么中序遍历获取结果
res.get(1).add(cur.val);
}
// 后序获取结果
res.get(2).add(cur.val);
// 后序遍历,栈中元素可以弹出了,因为不需要再回来了
stack.pop();
// 更新后序遍历上一次访问的节点
pre = cur;
// cur要置空,因为要返回上一层,即取栈中元素
cur = null;
} else {
// 第二次访问,中序遍历
res.get(1).add(cur.val);
cur = cur.right;
}
}
}
非递归版本的过程见图:
cur.right 并未被真正的访问到,或者说 cur.right == null
通过是否是null的判断,实际上也算一次访问了。
非递归版本遍历的简化
统一的模板是适用于所有的递归情况,如果只需要单独的某个版本是可以简化代码逻辑的。
如果是先序和中序遍历,那么只需要关心第一次访问和第二次访问即可,无需关心其他;而后序遍历比较麻烦,需要关心是否是第三次返回的情况,所以各个非递归遍历简化版代码如下:
public void preOrder(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
Deque<TreeNode> stack = new ArrayDeque<>();
while (!stack.isEmpty() || cur != null) {
//
while (cur != null) {
// 先序遍历,第一次访问该节点
res.get(0).add(cur.val);
stack.push(cur);
cur = cur.left;
}
// cur 此时为null
// 返回,即弹出栈中元素覆盖即可
cur = stack.pop();
// 如果right节点是null也无妨,会继续弹出
cur = cur.right;
}
}
public void inOrder(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
Deque<TreeNode> stack = new ArrayDeque<>();
while (!stack.isEmpty() || cur != null) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
// 中序遍历,第二次访问该节点
res.get(1).add(cur.val);
cur = cur.right;
}
}
public void postOrder(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
TreeNode pre = null;
Deque<TreeNode> stack = new ArrayDeque<>();
while (!stack.isEmpty() || cur != null) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
// 后序遍历比较麻烦,不能直接弹出,需要判断是否是第三次返回
cur = stack.peek();
if (cur.right == pre || cur.right == null) {
res.get(2).add(cur.val);
stack.pop();
pre = cur;
cur = null;
} else {
cur = cur.right;
}
}
}
Morris遍历
在有了对树的遍历的流程的认识后,简单介绍一下二叉树的Morris遍历。
正如我们所见,二叉树的遍历,需要用到栈,即空间复杂度是O(logn)
,时间复杂度是O(n).
Morris遍历可以实现二叉树空间复杂度为O(1),时间复杂度也是O(n),他的原理是通过利用原树中大量空闲指针的方式,达到节省空间的目的。
其基本的规则是:
假设访问cur节点:
- 如果 cur 没有左孩子,cur 向右移动 (cur = cur.right)
- 如果 cur 有左孩子,找到左孩子最右的非空节点 mostRight:
a. 如果 mostRight 的右指针指向空,让其指向 cur,然后 cur 向左移动 (cur = cur.left)
b. 如果 mostRight 的右指针指向 cur,让其指向 null,然后cur向右移动 (cur = cur.right) - cur 为空时遍历停止
根据规则,我们可以看到,通过 mostRight 节点的right指针,判断是第几次回到当前节点:
- 如果是指向null,那么说明是第一次来到该节点;
- 如果指向自己,那么说明是第二次回到当前节点。
Morris遍历并没有第三次回到当前节点的操作,所以需要逆序打印边界。
时间复杂度是O(N),因为对于树中的每个节点至多只能被访问两次。
遍历过程详情图和代码:
public class Morris {
public static void main(String[] args) {
TreeNode root = new TreeNode(4);
root.left = new TreeNode(2);
root.right = new TreeNode(6);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(3);
root.right.left = new TreeNode(5);
root.right.right = new TreeNode(7);
traverseTree(root);
System.out.println("=====");
morris(root);
}
public static void traverseTree(TreeNode root) {
if (root == null) {
return;
}
// 先序
// System.out.println(root.val);
traverseTree(root.left);
// 中序
// System.out.println(root.val);
traverseTree(root.right);
// 后序
System.out.println(root.val);
}
public static void morris(TreeNode root) {
if (root == null) {
return;
}
TreeNode cur = root;
while (cur != null) {
// 1. 没有左孩子,cur向右边移动
if (cur.left == null) {
// 这个也是第一次到,也是第二次到,对于先序和中序遍历而言,都需要处理
// System.out.println(cur.val);
cur = cur.right;
} else {
// 2 有左孩子,找到左孩子最右的非空节点 mostRight
TreeNode mostRight = cur.left;
while (mostRight.right != null && mostRight.right != cur) {
mostRight = mostRight.right;
}
// 判断 mostRight
if (mostRight.right == null) {
// a. mostRight 指向null,则让其指向自己,向左边走
mostRight.right = cur;
// 第一次到cur,先序遍历
// System.out.println(cur.val);
cur = cur.left;
} else {
// b. 如果 mostRight 的右指针指向 cur,让其指向 null,然后cur向右移动
mostRight.right = null;
// 第二次到cur,中序遍历
// System.out.println(cur.val);
// 后序遍历,左右中,相当于是打印树的 \ 这个样子的右斜线,所以在第二次离开的时候,逆序打印右斜线 \ 即可。
// 为了使用O(1)的空间复杂度,可以先对树的节点进行 "反转链表" 的操作,然后打印完成之后,再反转回来
printReverseRightEdge(cur.left);
cur = cur.right;
}
}
}
printReverseRightEdge(root);
}
private static void printReverseRightEdge(TreeNode root) {
if (root == null) {
return;
}
// 反转列表
TreeNode rHead = reverseRightEdge(root);
// 打印
TreeNode cur = rHead;
while (cur != null) {
System.out.println(cur.val);
cur = cur.right;
}
// 反转回来
reverseRightEdge(rHead);
}
private static TreeNode reverseRightEdge(TreeNode root) {
TreeNode pre = null;
TreeNode cur = root;
while (cur != null) {
TreeNode next = cur.right;
cur.right = pre;
pre = cur;
cur = next;
}
return pre;
}
}
参考资料
Morris 遍历图,参考:https://blog.csdn.net/qq_38636076/article/details/119147902
牛客网左神算法课程
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix