二叉树的递归层序遍历
二叉树的递归/层序遍历
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<>();// 根节点的路径权重和是 1queue.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. 二叉树的中序遍历 | 🟢 |