二叉树的遍历

遍历是数据结构中的常见操作(访问数据结构中的所有元素)。

线性数据结构的遍历比较简单,正序遍历或逆序遍历。而对于二叉树来讲,根据节点访问顺序的不同,可以分为四种:

  • 前序遍历(Preorder Traversal)
  • 中序遍历(Inorder Traversal)
  • 后序遍历(Postorder Traversal)
  • 层序遍历(Level Order Traversal)

下面我们来看看他们的特点。

前序遍历(Pre-Order Traversal)

前序遍历二叉树,就是先访问根结点(中间节点),再前序遍历左子树,最后前序遍历右子树

如上图:

我们再回想一下,前序遍历的访问顺序是什么?首先根节点肯定是最先被访问,然后是左子树,最后才是右子树。

因此在上图中,前序遍历的访问顺序如下:

  1. 访问根节点 F
  2. 访问左子树 B
    • 访问左子节点 A
    • 访问右子节点 D
      • 访问左子节点 C
      • 访问右子节点 E
  3. 访问右子树 G
    • 访问右子节点 I
      • 访问左子节点 H

前序遍历的顺序是 [F]->[B]->[A]->[D]->[C]->[E]->[G]->[I]->[H]。这表示在按照前序遍历方式访问树的时候,首先访问根节点 F,然后按照左子树和右子树的顺序递归进行访问。

由于树本身是一种递归定义的数据结构,因此很自然也可以用递归的方式来遍历。

用递归法对二叉树进行前序遍历:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 前序遍历,获取二叉树节点的值列表
* @param root 二叉树的根节点
* @return 二叉树节点值的列表
*/
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<Integer>();
preorder(root, result);
return result;
}

/**
* 用前序遍历的方式递归处理二叉树节点,并将节点值添加到结果列表中
* @param node 当前处理的二叉树节点
* @param result 存储节点值的列表
*/
public void preorder(TreeNode node, List<Integer> result) {
// 如果节点为空,直接返回
if (node == null) {
return;
}

// 将当前节点的值添加到结果列表中
result.add(node.val);

// 递归处理左子树
preorder(node.left, result);

// 递归处理右子树
preorder(node.right, result);
}

可以看到,递归的方式去前序遍历二叉树很简单,思路也很清晰。

如果使用迭代的方式实现前序遍历,可以使用栈数据结构来模拟递归的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 以迭代方式实现前序遍历二叉树节点的值列表
* @param root 二叉树的根节点
* @return 二叉树节点值的列表
*/
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if(root == null) return result;

// 使用栈来模拟递归的过程
Stack<TreeNode> stack = new Stack<>();

// 将根节点入栈
stack.push(root);

while (!stack.isEmpty()) {
// 出栈当前节点
TreeNode current = stack.pop();

// 将当前节点的值添加到结果列表中
result.add(current.val);

// 先将右子树入栈,再将左子树入栈
if(current.right != null) stack.push(current.right);
if(current.left != null) stack.push(current.left);
}
return result;
}

这个迭代的实现使用栈来模拟递归,首先将根节点(也就是中间节点)入栈,然后在循环中弹出当前节点,将其值添加到result中,并按照先右后左的顺序将右子树和左子树入栈。这样可以模拟前序遍历的顺序。

  • Q:为什么要先加入右节点,再加入左节点呢?
  • A:因为这样出栈的时候才是先左后右的顺序

中序遍历(In-Order Traversal)

中序遍历是先访问左节点,再访问根节点,最后访问右节点

以上图为例:

  • (1)从根节点 F 开始,按照中序遍历的规则,首先遍历F的左子树B。
    • 遍历B的左子树A
      • 遍历A的左子树,为空,跳过。
      • 遍历A的根节点,输出A
      • 遍历A的右子树,为空,跳过。
    • 遍历B的根节点,输出B
    • 遍历B的右子树D
      • 遍历D的左子树C
        • 遍历C的左子树,为空,跳过。
        • 遍历C的根节点,输出C
        • 遍历C的右子树,为空,跳过。
      • 遍历D的根节点,输出D
      • 遍历D的右子树E
        • 遍历E的左子树,为空,跳过。
        • 遍历E的根节点,输出E
        • 遍历E的右子树,为空,跳过。
  • (2)现在根节点 F 的左子树遍历完了,接着遍历F的根节点,输出F
  • (3)最后遍历F的右子树G
    • 遍历G的左子树,为空,跳过。
    • 遍历G的根节点,输出G
    • 遍历G的右子树I
      • 遍历I的左子树H。
        • 遍历H的左子树,为空,跳过。
        • 遍历H的根节点,输出H
        • 遍历H的右子树,为空,跳过。
      • 遍历I的根节点,输出I
      • 遍历I的右子树,为空,跳过。

遍历逻辑写的很清楚了,因此,中序遍历的顺序为 [A]->[B]->[C]->[D]->[E]->[F]->[G]->[H]->[I]

实际上,上图二叉树是一个二叉搜索树。我们发现,对于二叉搜索树来讲,中序遍历后的结果是升序的。这很有意思。由于在二叉搜索树中,左子树的值小于根节点的值,右子树的值大于根节点的值。所以结果是显而易见的。如果我们先访问右子树,再访问根结点,最后访问左子树,此时遍历的结果是降序的。

中序遍历实际上就是将根节点放在左子树和右子树的中间进行遍历,不一定非要先访问左子树,也可以先访问右子树,然后访问根节点,最后访问左子树。

  • Q:先访问右子树,然后访问根节点,最后访问左子树,也是中序遍历嘛?
  • A:二叉树的遍历实际上是一种基于根节点的划分,将根节点放在左右子树的中间进行遍历就算是中序遍历,因此不一定非要先访问左子树,也可以先访问右子树,然后访问根节点,最后访问左子树,根据我们的具体需求。

代码实现如下:

  • 递归法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 中序遍历,获取二叉树节点的值列表
* @param root 二叉树的根节点
* @return 二叉树节点值的列表
*/
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
inorder(root, result);
return result;
}

/**
* 用中序遍历的方式递归处理二叉树节点,并将节点值添加到结果列表中
* @param node 当前处理的二叉树节点
* @param result 存储节点值的列表
*/
public void inorder(TreeNode node, List<Integer> result) {
// 如果节点为空,直接返回
if (node == null) {
return;
}

// 递归处理左子树
inorder(node.left, result);

// 将当前节点的值添加到结果列表中
result.add(node.val);

// 递归处理右子树
inorder(node.right, result);
}
  • 迭代法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 中序遍历二叉树,返回一个包含节点值的列表。
*
* @param root 二叉树的根节点
* @return 中序遍历结果的列表
*/
public List<Integer> inorderTraversal(TreeNode root) {
// 创建一个存储结果的列表
List<Integer> result = new ArrayList<>();

// 如果根节点为空,直接返回空列表
if (root == null) {
return result;
}

// 创建一个栈用于辅助遍历
Stack<TreeNode> stack = new Stack<>();

// 当前节点初始化为根节点
TreeNode cur = root;

// 循环条件:当前节点不为空,或者栈不为空
while (cur != null || !stack.isEmpty()) {
// 如果当前节点不为空,将其入栈并移动到左子树
if (cur != null) {
stack.push(cur);
cur = cur.left;
} else {
// 如果当前节点为空,说明左子树已经遍历完毕
// 弹出栈顶节点,添加其值到结果列表
cur = stack.pop();
result.add(cur.val);

// 移动到右子树
cur = cur.right;
}
}

// 返回最终的中序遍历结果列表
return result;
}

后序遍历(Post-Order Traversal)

看完前两个遍历,我们也能猜出后序遍历的规则了。

没错,后序遍历的遍历顺序为左子树、右子树、根节点。具体来说,在后序遍历中,我们先遍历左子树,然后遍历右子树,最后访问根节点。

以上图为例:

  • (1)从根节点 F 开始,按照后序遍历的规则,首先遍历F的左子树B。
    • 遍历B的左子树A
      • 遍历A的左子树,为空,跳过。
      • 遍历A的右子树,为空,跳过。
      • 遍历A的根节点,输出A
    • 遍历B的右子树D
      • 遍历D的左子树C
        • 遍历C的左子树,为空,跳过。
        • 遍历C的右子树,为空,跳过。
        • 遍历C的根节点,输出C
      • 遍历D的右子树E
        • 遍历E的左子树,为空,跳过。
        • 遍历E的右子树,为空,跳过。
        • 遍历E的根节点,输出E
      • 遍历D的根节点,输出D
    • 遍历B的根节点,输出B
  • 现在根节点 F 的左子树遍历完了,接着遍历F的右子树G
    • 遍历G的左子树,为空,跳过。
    • 遍历G的右子树I
      • 遍历I的左子树H。
        • 遍历H的左子树,为空,跳过。
        • 遍历H的右子树,为空,跳过。
        • 遍历H的根节点,输出H
      • 遍历I的右子树,为空,跳过。
      • 遍历I的根节点,输出I
    • 遍历G的根节点,输出G
  • 现在根节点 F 的左右子树都遍历完了,最后再遍历F的根节点,输出F

因此,后序遍历的顺序为 [A]->[C]->[E]->[D]->[B]->[H]->[I]->[G]->[F]

代码实现如下:

  • 递归法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 后序遍历,获取二叉树节点的值列表
* @param root 二叉树的根节点
* @return 二叉树节点值的列表
*/
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
postorder(root, result);
return result;
}

/**
* 用中序遍历的方式递归处理二叉树节点,并将节点值添加到结果列表中
* @param node 当前处理的二叉树节点
* @param result 存储节点值的列表
*/
public void postorder(TreeNode node, List<Integer> result) {
// 如果节点为空,直接返回
if (node == null) {
return;
}

// 递归处理左子树
postorder(node.left, result);

// 递归处理右子树
postorder(node.right, result);

// 将当前节点的值添加到结果列表中
result.add(node.val);
}
  • 迭代法:

这里用到一个小技巧。和先序遍历类似,我们先访问根节点,然后遍历右子树,最后遍历左子树,此时遍历的顺序是根右左,因此我们将result反转后就得到了后序遍历的结果数组,即遍历顺序是左右根

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* 后序遍历二叉树,返回一个包含节点值的列表。
*
* @param root 二叉树的根节点
* @return 后序遍历结果的列表
*/
public List<Integer> postorderTraversal(TreeNode root) {
// 创建一个存储结果的列表
List<Integer> result = new ArrayList<>();

// 如果根节点为空,直接返回空列表
if (root == null) {
return result;
}

// 创建一个栈用于辅助遍历
Stack<TreeNode> stack = new Stack<>();

// 将根节点压入栈
stack.push(root);

// 循环条件:栈不为空
while (!stack.isEmpty()) {
// 弹出栈顶节点
TreeNode node = stack.pop();

// 将节点值添加到结果列表
result.add(node.val);

// 注意:这里与前序遍历的顺序相反
// 先将右子树压入栈,再将左子树压入栈
if (node.right != null) stack.push(node.right);
if (node.left != null) stack.push(node.left);
}

// 由于后序遍历是“左右根”的顺序,因此将“根右左”的顺序翻转即可
Collections.reverse(result);

// 返回最终的后序遍历结果列表
return result;
}

小结 | 前中后序遍历

让我们用 LDR 分别表示遍历左子树、访问根节点和遍历右子树。

那么:

  • D L R先序遍历二叉树的顺序(或者D R L
  • L D R中序遍历二叉树的顺序(或者R D L
  • L R D后序遍历二叉树的顺序(或者R L D

备注:我们主要观察访问根节点的访问时机,就可以判断属于哪种遍历。

层序遍历(Level Order Traversal)

最后是层序遍历。

层序遍历,就是从上到下,从左到右依次访问每一个节点。

如上图所示。

代码如下:

  • 递归形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 全局变量,用于存储最终结果
public List<List<Integer>> result = new ArrayList<>();

// 主函数,用于调用层序遍历方法并返回最终结果
public List<List<Integer>> levelOrderTraversal(TreeNode root) {
levelOrder(root, 0);
return result;
}

// 层序遍历方法,采用递归方式实现
public void levelOrder(TreeNode node, Integer level) {
// 如果当前节点为空,直接返回
if (node == null) return;

// 如果结果列表的大小小于等于当前层级,则需要添加新的层
if (result.size() <= level) {
List<Integer> item = new ArrayList<Integer>();
result.add(item);
}

// 将当前节点的值添加到对应层的列表中
result.get(level).add(node.val);

// 递归调用左子树和右子树,并将层级加一
levelOrder(node.left, level + 1);
levelOrder(node.right, level + 1);
}
  • 迭代形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 全局变量,用于存储最终结果
public List<List<Integer>> result = new ArrayList<>();

// 主函数,用于调用层序遍历方法并返回最终结果
public List<List<Integer>> levelOrderTraversal(TreeNode root) {
levelOrder(root);
return result;
}

// 层序遍历方法:迭代
public void levelOrder(TreeNode node) {
// 如果根节点为空,直接返回
if (node == null) return;

// 创建一个队列用于存储节点
Queue<TreeNode> que = new LinkedList<>();
// 将根节点入队
que.offer(node);

// 循环直到队列为空
while (!que.isEmpty()) {
// 用于存储当前层的节点值的列表
List<Integer> itemList = new ArrayList<>();
// 获取当前层的节点数量
int len = que.size();

// 遍历当前层的所有节点
while (len > 0) {
// 出队一个节点
TreeNode tmpNode = que.poll();
// 将节点值添加到当前层的列表中
itemList.add(tmpNode.val);

// 如果左子树不为空,将左子树节点入队
if (tmpNode.left != null) que.offer(tmpNode.left);
// 如果右子树不为空,将右子树节点入队
if (tmpNode.right != null) que.offer(tmpNode.right);

// 当前层节点数量减一
len--;
}

// 将当前层的节点值列表添加到最终结果中
result.add(itemList);
}
}

复习巩固