开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
前、中、后序的非递归遍历
写在前面
本文摘要
- 前序遍历
- 中序遍历
- 后序遍历
二叉树遍历的复习
- 上篇文章,我们了解了如何遍历一棵二叉树。常见的有四种方式:
- 前序、中序、后序、层序
- 其中除了层序遍历,其余的都是使用递归实现的,如下图所示:
- 这三个递归函数,思路确实很好想,代码也很精简
- 可是我们都知道,调用函数会开辟栈帧,消耗栈空间
- 而遍历基本上就是要访问所有元素,时间复杂度一般情况都为
O(n),而此递归函数的空间复杂度应该也差不多为O(n) - 能不能优化一下这三个遍历的方式,不用递归来实现呢?
- 这就是我们今天要谈的内容,有如下一棵二叉树:
一、前序遍历
规律探寻
- 如果利用前序遍历,那么访问该树的顺序为:
20、15、10、5、12、18、25。这一点相信大家都清楚 - 有什么规律呢?我们一起来研究一下:
-
首先要明确的点:一开始只能拿到根节点
-
可以发现,前序遍历的顺序,刚好就是拿到根节点,一直往左走。走到为空为止
-
譬如上图:
- 先访问 20,20的左边不为空
- 访问15,15的左边不为空
- 访问10,10的左边不为空
- 访问5,5的左边为空
-
左边为空之后,该访问右边了。可以发现,左边最后访问的元素,它的右边会最先被访问(如果有)。左边最先被访问的元素,它的右边会最后被访问(如果有)
-
譬如上图:
- 访问完5之后,发现左边为空了,该访问它的右边了,它的右边为空
- 向上访问10的右边,不为空,访问12
- 访问12时,12的左边为空,右边也为空
- 向上访问15的右边,不为空,访问18
- 访问18时,18的左边为空,右边也为空
- 向上访问20的右边,不为空,访问25
- 访问25时,25的左边为空,右边也为空
- 至此,所有元素访问完毕
-
如果你感到晕,没关系,我说了这么半天,就是想让你注意到:先被访问的元素,它的右子节点反而会后被访问。后被访问的元素,它的右子节点反而会先被访问
-
这和什么有点像来着?是不是和
栈Stack的思想很像:先进后出
思路分析
- 先准备一个栈:
- 访问节点的同时,将它的右子节点入栈(如果有),直至左边为空:
- 左边为空后,弹出栈顶元素,重复上述逻辑:
代码实现
- 将其思路转换为代码:
public void preorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
Node<E> node = root;
Stack<Node<E>> stack = new Stack<>(); // 准备一个栈
while (true) {
if (node != null) { // 如果 node 没有到最左边
if (visitor.visit(node.element)) return; // 访问元素,若外界需要停止,直接返回即可
if (node.right != null) { // 右子节点不为空,将其入栈
stack.push(node.right);
}
node = node.left; // 再向左走
} else { // 处理右边
if (stack.isEmpty()) return; // 如果 node 和 栈 都为空了,就直接返回
node = stack.pop(); // 将栈顶元素弹出来,让 node 再次不为 null,执行上面的逻辑
}
}
}
实现思路2
- 前序遍历比较特殊,还有一种非递归的实现思路:
- 准备一个栈,将根节点直接入栈
- 当栈不为空时,遍历栈:
- 取出栈顶元素,执行访问逻辑
- 如果栈的右子节点不为空,将右子节点入栈
- 如果栈的左子节点不为空,将左子节点入栈
- 将其思路转换为代码:
public void preorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
// 准备一个栈,并且将根节点放入其中
Stack<Node<E>> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
final Node<E> node = stack.pop(); // 弹出栈顶元素
if (visitor.visit(node.element)) return; // 访问元素,若外界需要停止,直接返回即可
if (node.right != null) { // 如果右子节点不为空,将其入栈
stack.push(node.right);
}
if (node.left != null) { // 如果左子节点不为空,将其入栈
stack.push(node.left);
}
}
}
- 哈哈哈,有没有感觉这样清晰很多,而且,它和之前学习的层序遍历是不是很像:
二、中序遍历
规律探寻
- 如果利用中序遍历,那么访问该树的顺序为:
5、10、12、15、18、20、25 - 我们来探寻一下规律:
- 首先也是要明确:一开始只能拿到根节点
- 可以发现,中序遍历的顺序,也是拿到根节点,走到最左边。最先访问最左边的元素
- 有了上面的铺垫,不难发现,这里也可以使用栈的思想来实现:先进后出
思路分析
- 先准备一个栈:
- 用一个临时引用:
current,当它不为空,将它入栈,然后一路向左:
- 到最左边了,栈顶元素不为空,弹出栈顶元素进行访问。如果它的右边不为空,将它赋值给
current,重复上述逻辑。直至栈为空。current也为空
代码实现
- 将其思路转换为代码:
public void inorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
Node<E> node = root;
Stack<Node<E>> stack = new Stack<>(); // 准备一个栈
while (true) {
if (node != null) { // node 不为空
stack.push(node); // 将自己入栈
node = node.left; // 一直往左边走,直到左边为 null
} else { // 处理右边
if (stack.isEmpty()) return; // 如果 stack 也为空,直接返回
final Node<E> pop = stack.pop(); // 弹出栈顶元素
if (visitor.visit(pop.element)) return; // 访问元素,若外界需要停止,直接返回即可
if (pop.right != null) { // 若栈顶元素的左子节点不为空
node = pop.right; // 让 node 再次不为 null,执行上面的逻辑
}
}
}
}
三、后序遍历
规律探寻
-
如果利用后序遍历,那么访问该树的顺序为:
5、12、10、18、15、25、20 -
我们来探寻一下规律:
- 首先也是要明确:一开始只能拿到根节点
- 可以发现,后序遍历的顺序,也是拿到根节点,走到最左边。最先访问最左边的元素
- 20是最早能够拿到的元素,但是最后才能访问它。这里也可以使用栈的思想来实现:先进后出
思路分析
- 先准备一个栈,并且直接将根节点入栈。再准备一个
prev节点,用于记录前一个访问的节点
- 开始遍历,直至栈为空
- 查看栈顶元素,看它是否为叶子节点或者是否是
prev的父节点。(仅查看,并未出栈) - 如果是,将其栈顶弹出,赋值给
prev节点。然后进行访问。 - 如果不是,查看它的左和右是否为空,如果不为空,将其入栈。
- 查看栈顶元素,看它是否为叶子节点或者是否是
代码实现
public void postorder(Visitor<E> visitor) {
if (visitor == null || root == null) return;
Stack<Node<E>> stack = new Stack<>();
stack.push(root); // 直接将根节点入栈
Node<E> prev = null; // 用于记录上一个被访问的元素
while (!stack.isEmpty()) {
final Node<E> peek = stack.peek(); // 只是看看栈顶元素,并不是弹出
// 如果栈顶元素是叶子节点 或 前一个访问的父节点是栈顶元素
if (peek.isLeaf() || (prev != null && prev.parent == peek)) {
prev = stack.pop(); // 弹出栈顶元素
if (visitor.visit(prev.element)) return; // 访问元素,若外界需要停止,直接返回即可
} else {
if (peek.right != null) { // 栈顶元素的右子节点不为空,将其入栈
stack.push(peek.right);
}
if (peek.left != null) { // 栈顶元素的右子左子节点不为空,将其入栈
stack.push(peek.left);
}
}
}
}
四、总结
-
其实三种遍历方式,我都说了一个需要注意的地方:最开始只能拿到根节点。所以我们只能从根节点开始,从上往下找
"虽然不同顺序的遍历各有各的不同,但是遍历的思想都是相似的"
-
都是准备一个栈
Stack,然后先处理左边。看遍历的顺序,将它的相关节点入栈 -
最后再处理右边,直至栈为空,退出循环
-
当然,你也可以试试先处理右边,再处理左边~
写在后面
本篇收获
- 复习了二叉树的遍历
- 复习了栈的使用
- 学习了非递归遍历二叉树的方式