algorithm-base/animation-simulation/二叉树/二叉树的前序遍历(栈).md
2021-05-21 18:34:13 +08:00

173 lines
7.1 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

我们之前说了二叉树基础及二叉的几种遍历方式及练习题今天我们来看一下二叉树的前序遍历非递归实现
前序遍历的顺序是, 对于树中的某节点,`先遍历该节点,然后再遍历其左子树,最后遍历其右子树`.
我们先来通过下面这个动画复习一下二叉树的前序遍历
![前序遍历](https://img-blog.csdnimg.cn/20210504155755565.gif)
### 迭代
我们试想一下之前我们借助队列帮我们实现二叉树的层序遍历
那么可不可以也借助数据结构帮助我们实现二叉树的前序遍历
见下图
![image](https://cdn.jsdelivr.net/gh/tan45du/test@master/image.622242fm7dc0.png)
假设我们的二叉树为 [1,2,3]我们需要对其进行前序遍历其遍历顺序为
当前节点 1左孩子 2右孩子 3
这里可不可以用栈帮我们完成前序遍历呢
> 栈和队列的那些事
我们都知道栈的特性是先进后出我们借助栈来帮助我们完成前序遍历的时候
则需要注意的一点是我们应该`先将右子节点入栈再将左子节点入栈`
这样出栈时则会先出左节点再出右子节点则能够完成树的前序遍历
见下图
![](https://img-blog.csdnimg.cn/20210512205822221.gif)
我们用一句话对上图进行总结`当栈不为空时栈顶元素出栈如果其右孩子不为空则右孩子入栈其左孩子不为空则左孩子入栈`还有一点需要注意的是我们和层序遍历一样需要先将 root 节点进行入栈然后再执行 while 循环
看到这里你已经能够自己编写出代码了不信你去试试
时间复杂度O(n) 需要对所有节点遍历一次
空间复杂度O(n) 栈的开销平均为 O(logn) 最快情况即斜二叉树时为 O(n)
**参考代码**
```java
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
if (root == null) return list;
stack.push(root);
while (!stack.isEmpty()) {
TreeNode temp = stack.pop();
if (temp.right != null) {
stack.push(temp.right);
}
if (temp.left != null) {
stack.push(temp.left);
}
//这里也可以放到前面
list.add(temp.val);
}
return list;
}
}
```
### Morris
Morris 遍历利用树的左右孩子为空大量空闲指针实现空间开销的极限缩减这个遍历方法稍微有那么一丢丢难理解不过结合动图也就一目了然啦下面我们先看动画吧
看完视频是不是感觉自己搞懂了又感觉自己没搞懂哈哈咱们继续往下看
![image](https://cdn.jsdelivr.net/gh/tan45du/test@master/image.1u3at0ckvn34.png)
我们之前说的Morris 遍历利用了`树中大量空闲指针的特性`我们需要`找到当前节点的左子树中的最右边的叶子节点`将该叶子节点的 right 指向当前节点例如当前节点为2其左子树中的最右节点为 9 则在 9 节点添加一个 right 指针指向 2
其实上图中的 Morris 遍历遵循两个原则我们在动画中也能够得出
1. p1.left == null p1 = p1.right(这也就是我们为什么要给叶子节点添加 right 指针的原因)
2. 如果 p1.left != null找到 p1 左子树上最右的节点(也就是我们的 p2 最后停留的位置)此时我们又可以分为两种情况一种是叶子节点添加 right 指针的情况一种是去除叶子节点 right 指针的情况
3. - 如果 p2 right 指针指向空让其指向 p1p1向左移动, p1 = p1.left
- 如果 p2 right 指针指向 p1让其指向空为了防止重复执行则需要去掉 right 指针p1 向右移动p1 = p1.right
这时你可以结合咱们刚才提到的两个原则再去看一遍动画并代入规则进行模拟差不多就能完全搞懂啦
下面我们来对动画中的内容进行拆解
首先 p1 指向 root节点
p2 = p1.left下面我们需要通过 p2 找到 p1的左子树中的最右节点即节点 5然后将该节点的 right 指针指向 root并记录 root 节点的值
![image](https://cdn.jsdelivr.net/gh/tan45du/test@master/image.3h60vcjhqo80.png)
向左移动 p1 p1 = p1.left
p2 = p1.left 即节点 4 找到 p1 的左子树中的最右叶子节点也就是 9并将该节点的 right 指针指向 2
![image](https://cdn.jsdelivr.net/gh/tan45du/test@master/image.zq91mdjkyzk.png)
继续向左移动 p1, p1 = p1.leftp2 = p1.left 也就是节点 8并将该节点的 right 指针指向 p1
![image](https://cdn.jsdelivr.net/gh/tan45du/test@master/image.5vsh71yrzxs0.png)
我们发现这一步给前两步是一样的都是找到叶子节点将其 right 指针指向 p1,此时我们完成了添加 right 指针的过程下面我们继续往下看
我们继续移动 p1 指针p1 = p1.leftp2 = p.left此时我们发现 p2 == null,即下图
![image](https://cdn.jsdelivr.net/gh/tan45du/test@master/image.zk7nxrjdgr.png)
此时我们需要移动 p1, 但是不再是 p1 = p1.left 而是 p1 = p1.right也就是 4继续让 p2 = p1.left此时则为下图这种情况
![image](https://cdn.jsdelivr.net/gh/tan45du/test@master/image.1pjni9r6tkps.png)
此时我们发现 p2.right != null 而是指向 4说明此时我们已经添加过了 right 指针所以去掉 right 指针并让 p1 = p1.right
![image](https://cdn.jsdelivr.net/gh/tan45du/test@master/image.17t7n8yy340w.png)
下面则继续移动 p1 ,按照规则继续移动即可遇到的情况已经在上面做出了举例所以下面我们就不继续赘述啦如果还不是特别理解的同学可以再去看一遍动画加深下印象
时间复杂度 On空间复杂度 O1
下面我们来看代码吧
#### 代码
```java
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> list = new ArrayList<>();
if (root == null) {
return list;
}
TreeNode p1 = root; TreeNode p2 = null;
while (p1 != null) {
p2 = p1.left;
if (p2 != null) {
//找到左子树的最右叶子节点
while (p2.right != null && p2.right != p1) {
p2 = p2.right;
}
//添加 right 指针,对应 right 指针为 null 的情况
if (p2.right == null) {
list.add(p1.val);
p2.right = p1;
p1 = p1.left;
continue;
}
//对应 right 指针存在的情况,则去掉 right 指针
p2.right = null;
} else {
list.add(p1.val);
}
//移动 p1
p1 = p1.right;
}
return list;
}
}
```
好啦今天就看到这里吧咱们下期见