二叉树的遍历
对于线性数据结构(Array、Linked List、Queues、Stacks等),一般只能通过一种逻辑方式来进行遍历,但是树比较不同,我们可以用不同的方式遍历:
- 深度优先
- 先序遍历: 1, 2, 3, 5, 4
- 中序遍历: 3, 2, 5, 1, 4
- 后序遍历: 3, 5, 2, 4, 1
- 广度优先(层序遍历): 1, 2, 4, 3, 5
这篇文章主要关注深度优先遍历的部分,对于每种遍历,我们都可以采用递归和迭代(非递归)的方式。
首先我们定义一下Tree中的Node类
1
2
3
4
5
6
7
8
9
|
class Node {
int key;
Node left, right;
public Node(int item) {
key = item;
left = right = null;
}
}
|
先序遍历
递归方式
思路
1
2
3
4
|
1 − 访问根节点
2 − 递归遍历左子树
3 − 递归遍历右子树
直到访问所有节点
|
代码
1
2
3
4
5
6
7
8
|
void recursivePreorder(Node node)
{
if (node == null) return;
System.out.print(node.key + " ");
recursivePreorder(node.left);
recursivePreorder(node.right);
}
|
非递归方式
思路
我们知道在计算机系统中,递归其实是在调用栈的基础上实现的,为了将一个递归过程转换为迭代过程,我们需要显式维护一个栈。以下是基于栈的先序遍历迭代过程:
1
2
3
4
5
6
7
|
1 - 创建一个空栈nodeStack,并将根节点入栈
2 - 在nodeStack不为空的情况下执行以下操作
a - 从堆栈中弹出一个节点并打印出来
b - 将弹出节点的右子节点入栈
c - 将弹出节点的左子节点入栈
(右子节点在左子节点之前入栈,以确保左子树先被处理)
|
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
void iterativePreorder(Node root) {
if (root == null) return;
Stack<Node> nodeStack = new Stack<Node>();
nodeStack.push(root);
while (!nodeStack.empty()) {
Node current = nodeStack.pop();
System.out.print(current.key + " ");
if (current.right != null) nodeStack.push(current.right);
if (current.left != null) nodeStack.push(current.left);
}
}
|
中序遍历
递归方式
思路
1
2
3
4
|
1 − 递归遍历左子树
2 − 访问根节点
3 − 递归遍历右子树
直到访问所有节点
|
代码
1
2
3
4
5
6
7
8
|
void recursiveInorder(Node node)
{
if (node == null) return;
recursiveInorder(node.left);
System.out.print(node.key + " ");
recursiveInorder(node.right);
}
|
非递归方式
思路
1
2
3
4
5
6
7
8
|
1 - 创建一个空栈nodeStack和一个Node变量current
并将current初始化为根节点(直接用root亦可,此处只是为了命名清晰)
2 - 将current入栈,设置current = current.left,直到current为null
3 - 如果current为null,且堆栈不是空的,则
a - 从堆栈中弹出最上面的节点popped_node
b - 打印弹出的节点,设置current = popped_node.right
c - 重复步骤2
4 - 直到current为null,且栈为空,结束
|
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
void iterativeInorder(Node root) {
if (root == null) return;
Stack<Node> nodeStack = new Stack<Node>();
Node current = root;
while (current != null || !nodeStack.empty()) {
if (current != null) {
nodeStack.push(current);
current = current.left;
} else {
current = nodeStack.pop();
System.out.print(current.key + " ");
current = current.right;
}
}
}
|
后序遍历
递归方式
思路
1
2
3
4
|
1 − 递归遍历左子树
2 − 递归遍历右子树
3 − 访问根节点
直到访问所有节点
|
代码
1
2
3
4
5
6
7
8
|
void recursivePostorder(Node node)
{
if (node == null) return;
recursivePostorder(node.left);
recursivePostorder(node.right);
System.out.print(node.key + " ");
}
|
非递归方式(使用一个栈)
后序遍历的非递归实现比上边两个遍历复杂一点,同时方法也比较多,我们逐个来看。
思路1
思路1是的中心思想是:
和中序遍历一样,先左指针向下移动到最左边的节点,不同的是我们需要一个标记位pre
用来记忆上一次打印(访问)的节点。
当我们到达最左边的节点时,如果它没有右子节点或者右子节点等于pre
,说明右子树已经遍历完了,打印它。
1
2
3
4
5
6
7
8
9
10
|
1 - 创建一个空栈nodeStack和一个Node变量current,并将current初始化为根节点
2 - 执行以下操作直到current为null
a - 将current入栈
b - 设置current = current.left
3 - 将current设置为栈顶元素
a - 如果 current.right等于null或者pre,意味着我们已经遍历了右边的子树,则:
- 打印curren,并将其出栈
- 令 pre=current; current=null(这样我们就不会再往下看左边的孩子了)
b - 否则令 current=current.right(先遍历右子树)
4 - 直到current为null,且栈为空,结束
|
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
void iterativePostorder1(Node root) {
if (root == null) return;
Stack<Node> nodeStack = new Stack<Node>();
Node current = root, pre = null;
while (current != null || !nodeStack.empty()) {
if (current != null) {
nodeStack.push(current);
current = current.left;
} else {
current = nodeStack.peek();
if (current.right == null || current.right == pre) {
System.out.print(current.key + " ");
nodeStack.pop();
pre = current;
current = null;
} else {
current = current.right;
}
}
}
}
|
思路2
有了思路1的基础,我们来看看它的变体:
在向左遍历的时候,直接将current
节点入栈两次。
在弹出的时候如果发现stack.peek()
和current
一样,那么就继续current.right
,否则就打印current
。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
void iterativePostorder2(Node root) {
if (root == null) return;
Stack<Node> nodeStack = new Stack<Node>();
Node current = root;
while (true) {
if (current != null) {
nodeStack.push(current);
nodeStack.push(current);
current = current.left;
} else if (nodeStack.empty()) {
return;
} else {
current = nodeStack.pop();
if (!nodeStack.empty() && current == nodeStack.peek()) {
current = current.right;
} else {
System.out.print(current.key + " ");
current = null;
}
}
}
}
|
非递归方式(使用两个栈)
对于后序遍历的非递归方式,我们也可以用两个栈,这个方法有点类似于非递归的先序遍历,但有两点主要区别:
- 不打印节点,而是把它push到另一个堆栈里
- 先入栈左子节点,后入栈右子节点
思路
1
2
3
4
5
|
1 - 将根节点push到第一个堆栈
2 - 在第一个堆栈不为空时进行循环:
a - 从第一个堆栈中弹出一个节点,并将其push到第二堆栈中
b - 将弹出的节点的左右子节点push到第一个堆栈中
3 - 打印第二个堆栈的内容
|
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
void iterativePostorder3(Node root) {
if (root == null) return;
Stack<Node> nodeStack1 = new Stack<Node>();
Stack<Node> nodeStack2 = new Stack<Node>();
nodeStack1.push(root);
while (!nodeStack1.empty()) {
Node current = nodeStack.pop();
nodeStack2.push(current);
if (current.left != null) nodeStack1.push(current.left);
if (current.right != null) nodeStack1.push(current.right);
}
while (!nodeStack2.empty()) {
Node temp = nodeStack2.pop();
System.out.print(temp.key + " ");
}
}
|
总结
二叉树的遍历很有趣也很有用,它既简单又复杂,简单在于思路上,复杂在于多变和细节上,其实二叉树的遍历还有其他的方法,以后有机会再补充。
从上边的描述中可以看出,非递归过程其实和递归过程的本质其实没什么区别,只不过对于非递归,我们需要自己维护一个堆栈来模拟操作系统里的递归函数调用栈。
文章作者
杂毛小道
上次更新
2021-01-18
许可协议
署名 4.0 国际 (CC BY 4.0)