二叉树的递归/层序遍历
1. 递归遍历(DFS)
递归遍历二叉树的代码如下:
class TreeNode {
int val;
TreeNode left, right;
}
public void traverse(TreeNode root) {
if (root == null) {
return;
}
// 进行遍历
traverse(root.left);
traverse(root.right);
}
traverse 函数的遍历顺序,就是一直往左节点走,左节点走不下去了,再往右节点走,然后又开始往左节点走,循环往复,直到左右节点都遍历完了,就返回父节点。看代码也能看出来,先调用 root.left,再调用 root.right,一定是先往左节点走,碰到 null 才开始往右节点走一次。
只要遵循先左再右的规则,traverse 函数访问节点的顺序就是固定的,插入多少条数据都不会发生变化。那有人问了,为什么之前学的数据结构会分为,前序遍历、中序遍历、后序遍历等不同的方式,这是为什么?
所谓的前序遍历这些方式,指的是在不同的时机将对节点进行操作(例如打印节点)。
public void traverse(TreeNode root) {
if (root == null) {
return;
}
//前序遍历
traverse(root.left);
// 中序遍历
traverse(root.right);
// 后序遍历
}
前序位置的代码会在进入节点时立即执行;中序位置的代码会在左子树遍历完成后,遍历右子树之前执行;后序位置的代码会在左右子树遍历完成后执行
特别强调,三种位置的不同关键是操作的时机不同,而二叉树遍历的的轨迹不会发生变化(先左再右)。在实际的算法题中,不会要求你写前中后序遍历的,关键在于把正确的代码写到正确的位置。
2. 层序遍历(BFS)
层序遍历,顾名思义,一层一层的进行遍历,从上到下,从左到右。层序遍历需要依赖队列来实现,可以分为三种方式来实现。补个关于队列的 API 的小知识点:
- add(E e):向队尾添加元素;队列满时抛异常。
- offer(E e):向队尾添加元素;队列满时返回 false(不抛异常)。
- remove():移除并返回队头元素;队列空时抛异常。
- poll():移除并返回队头元素;队列空时返回 null(不抛异常)。
- element():返回队头元素(不移除);队列空时抛异常。
- peek():返回队头元素(不移除);队列空时返回 null。
写法一
public void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
// 开始将头部的元素移除
TreeNode cur = queue.poll();
System.out.println(cur.val);
// 随后将 cur 的左右节点放到队列中
if (cur.left != null) {
q.offer(cur.left);
}
if (cur.right != null) {
q.offer(cur.right);
}
}
}
这种写法是最简单的,每次先将队头元素取出来,再将其左右节点放到队列尾部,就成了,但是缺点也明显,无法得知当前节点的在哪一层,这个需求挺常见的,比如:收集某一层的节点,或者找出二叉树的最小深度等等。
写法二
public void levelOrderTraverse(TreeNode root) {
if (root == null) {
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
// 记录当前遍历到的层数(根节点视为第 1 层)
int depth = 1;
while (!queue.isEmpty()) {
// 可以使用一个 size() 方法,得知当前队列内有多少元素,这些元素都在一层的
int sz = queue.size();
for (int i = 0; i < sz; i++) {
TreeNode cur = queue.poll();
// 访问 cur 节点,同时打印 cur 所在的位置(哪一层depth)
System.out.println("depth = " + depth + ", val = " + cur.val);
// 将 cur 的左右节点放到队尾
if (cur.left != null) {
queue.offer(cur.left);
}
if (cur.right != null) {
queue.offer(cur.right);
}
}
depth++;
}
}
这种写法就可以记录下来每个节点所在的层数,可以解决诸如二叉树最小深度这样的问题,是我们最常用的层序遍历写法。
写法三
回顾一下写法二,每往下走一层,depth 进行+1操作,是不是可以理解为每条树枝的权重是1,二叉树的每个节点是不是可以理解为:根节点到该节点的路径权重和,但是吧,同一层的节点,它们的路径权重和是不是一样的?
那么假设,每个节点的路径权重和是任意数,让你层序遍历每个节点,打印其的路径权重和,你会怎么办?那么,每一层的节点的路径权重和就和之前不一样了,写法二这种只使用一个 depth 变量就无法满足需求了。
写法三就是为了解决这个问题,在方法一的基础上写一个 State 类,让每个节点自己负责维护自身的路径权重。
class State {
TreeNode node;
int depth;
State(TreeNode node, int depth) {
this.node = node;
this.depth = depth;
}
}
public void levelOrderTarverse(TreeNode root) {
if (root == null) {
return;
}
Queue<State> queue = new LinkedList<>();
// 根节点的路径权重和是 1
queue.offer(new State(root, 1));
while (!queue.isEmpty()) {
State cur = queue.poll();
// 再访问 cur 节点的同时,打印其权重路径和
System.out.println("depth:" + cur.depth + ",val = " + cur.node.val);
// 再将 cur 左右节点放到队尾中
if (cur.node.left != null) {
queue.offer(new State(cur.node.left, cur.depth + 1));
}
if (cur.node.right != null) {
queue.offer(new State(cur.node.right, cjur.depth + 1));
}
}
}
这样每个节点都有了自己的 depth 变量,是最灵活的,可以满足所有 BFS 算法的需求。但是由于要额外定义一个 State 类比较麻烦,所以非必要的话,用写法二就够了。
接下来,你就可以无痛完成下面这几道题了
| 力扣 | 难度 |
|---|---|
| 102. 二叉树的层序遍历 | 🟠 |
| 144. 二叉树的前序遍历 | 🟢 |
| 145. 二叉树的后序遍历 | 🟢 |
| 94. 二叉树的中序遍历 | 🟢 |
6497




被折叠的 条评论
为什么被折叠?



