先序、中序、后序遍历
我们可以将二叉树分成三个部分 ------ 根节点 与 左右子树 ,其中左右子树分别又是二叉树。
二叉树的遍历是要以某种顺序依次访问二叉树中所有节点,根据访问这三部分顺序不同产生了不同遍历方式。
由于二叉树是有序树,所以访问左右子树相对顺序通常是不变的,即左子树总在右子树之前访问。
访问顺序能变的就只有根节点了,如果根节点在左右子树之前访问,那么我们称这种遍历方式为 先序遍历 (根左右),如果根节点在左右子树之间访问,就为 中序遍历 (左根右),如果根节点在左右子树之后访问,就为 后序遍历 (左右根)。
有时候,先序遍历也叫先根遍历 / 先序周游,中序遍历和后序遍历同理。
另外,由于二叉树是 递归 定义的,所以也要以相同顺序递归访问左右子树时。
1. 先序遍历
下图展示了先序遍历一棵二叉树过程
以上过程可用如下代码实现
java
List<Integer> preorder = new ArrayList<>(); // 遍历结果
// 先序遍历
public void dfs(TreeNode root) {
if (root == null) { // 空树不访问
return;
}
preorder.add(root.val); // ① 访问根节点
dfs(root.left); // ② 先序遍历左子树
dfs(root.right); // ③ 先序遍历右子树
}
2. 中序遍历
下图展示了中序遍历同一棵二叉树过程
可用如下代码实现
java
List<Integer> inorder = new ArrayList<>(); // 遍历结果
public void dfs(TreeNode treeNode) {
if (treeNode == null) { // 空树不访问
return;
}
dfs(treeNode.left); // ① 中序遍历左子树
inorder.add(treeNode.val); // ② 访问根节点
dfs(treeNode.right); // ③ 中序遍历右子树
}
3. 后序遍历
下图展示了后序遍历同一棵二叉树过程
可用如下代码实现
java
List<Integer> postorder = new ArrayList<>(); // 遍历结果
public void dfs(TreeNode treeNode) {
if (treeNode == null) { // 空树不访问
return;
}
dfs(treeNode.left); // ① 后序遍历左子树
dfs(treeNode.right); // ② 后序遍历右子树
postorder.add(treeNode.val); // ③ 访问根节点
}
层序遍历
二叉树的 层序遍历 是对二叉树所有节点以 "从上至下,从左至右" 顺序 逐层 地进行访问。
下图展示了对二叉树 层序遍历 过程
从上图可以看出, 层序遍历 实质上是对二叉树进行 广度优先搜索, 所以我们可以套用广搜模板代码完成二叉树的层序遍历。
java
public List<Integer> levelOrder(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
List<Integer> list = new ArrayList<>();
if (root != null) {
queue.offer(root); // ① 搜索起点入队
}
while (!queue.isEmpty()) {
TreeNode head = queue.poll();
list.add(head.val); // ② 访问
if (head.left != null) {
queue.add(head.left); // ③ 左孩子入队
}
if (head.right != null) {
queue.add(head.right); // ④ 右孩子入队
}
}
return list;
}
该算法是把二叉树层序遍历节点拉成 "一条线",并没有体现层次结构,在很多情况下是不够用的。
为了区分层次,我们需要在该算法基础上进改进 ------ 逐层推演。
java
public List<List<Integer>> levelOrder(TreeNode root) {
int level = 1; // 层号
// 层号 → 该层从左至右所有节点
TreeMap<Integer, List<TreeNode>> map = new TreeMap<>();
List<TreeNode> currentLevel = new ArrayList<>(); // 当前层
if (root != null) {
currentLevel.add(root); // ① 初始时, 当前层为根节点所在的第一层
}
while (!currentLevel.isEmpty()) {
map.put(level++, currentLevel);
List<TreeNode> nextLevel = new ArrayList<>(); // 下一层
for (TreeNode treeNode : currentLevel) { // ② 通过当前层推演下一层
if (treeNode.left != null) {
nextLevel.add(treeNode.left);
}
if (treeNode.right != null) {
nextLevel.add(treeNode.right);
}
}
// ③ 下一层变为当前层, 循环推演下下层
currentLevel = nextLevel;
}
return map.values().stream()
// TreeNode → val
.map(k -> k.stream().map(q -> q.val).collect(Collectors.toList()))
.collect(Collectors.toList());
}
这样,每一层节点都从左至右保存到 TreeMap<Integer, List<TreeNode>> map
,想要计算 每层 / 某层 特性时,只需要操作 map
即可。例如,
1. 计算二叉树每层 最大值 / 最小值 / 平均值 / 总和 等聚合特性
java
map.values().stream()
// 将每层节点转换为 IntStream ,
// 然后调用 IntStream 的 sum() / min() / max() / count() / average() 等聚合函数
// 即可求出二叉树每一层节点的 总和 / 最小值 / 最大值 / 数量 / 平均值 等等
.map(k -> k.stream().mapToInt(v -> v.val).max().orElse(0))
.collect(Collectors.toList());
2. 二叉树 左 / 右 视图
什么是二叉树的 左 视图呢?
二叉树的 左 视图即为从左边看二叉树,这样每一层第一个节点会遮挡着一层右边节点,所以只能看到每一层的第一个节点,相当于求 每一层第一个节点 。 右 视图类似,相当于求 每一层最后一个节点 。
java
// 左视图
map.values().stream()
// 每一层第一个节点
.map(k -> k.stream().mapToInt(v -> v.val).findFirst().orElse(0))
.collect(Collectors.toList());
java
// 右视图
map.values().stream()
// 每一层最后一个节点
.map(k -> k.stream().mapToInt(v -> v.val).reduce((f, s) -> s).orElse(0))
.collect(Collectors.toList());
3. 二叉树的 S 形遍历
二叉树的 S 形遍历是指对二叉树的第一层节点从左至右访问,第二层从右至左访问,第三层从左至右访问,以此类推,这样每一层变换访问方向,如下图。
对于二叉树的 S 形遍历,我们只需要在层序基础上将偶数层逆序即可,
java
map.forEach( (k, v) -> {
if(k % 2 == 0){ // 偶数层节点逆序
Collections.reverse(v);
}
});
垂直遍历
二叉树的 垂直遍历 是对二叉树所有节点以 "从左至右,从上至下" 顺序 逐列 地进行访问。
如下图二叉树 垂直遍历 序列为
11 → 2 → 7 → 1 → 4 → 5 → 12 → 3 → 8 → 9 → 6 → 13 → 10
为了实现二叉树的 垂直遍历 ,我们给二叉树每一列编号,根节点为第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0 列,根节点的左孩子为第 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 1 -1 </math>−1 列,根节点的右孩子为第 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 列,以此类推,节点向左走列号 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 1 -1 </math>−1, 向右走列号 <math xmlns="http://www.w3.org/1998/Math/MathML"> + 1 +1 </math>+1 。
由于要求列节点需要从上至下访问,我们可以通过 层 序遍历二叉树所有节点,并将其加入到对应列集合中。
java
public List<List<Integer>> verticalOrder(TreeNode root) {
// 队列中要存储节点以及节点所在列列号两个属性
// Java 核心类库未提供类似 C++ STL 的 Pair 类, 这里用 Map.Entry 替代
Queue<Map.Entry<Integer, TreeNode>> queue = new LinkedList<>();
// 列号 → 该列从上至下所有节点
TreeMap<Integer, List<Integer>> map = new TreeMap<>();
if (root != null) {
queue.offer(new SimpleImmutableEntry<>(0, root)); // ① 根节点入队
}
while (!queue.isEmpty()) {
Map.Entry<Integer, TreeNode> entry = queue.poll();
TreeNode front = entry.getValue();
// ② 访问
List<Integer> list = map.computeIfAbsent(entry.getKey(), k -> new ArrayList<>());
list.add(front.val);
if (front.left != null) {
// ③ 向左
queue.add(new SimpleImmutableEntry<>(entry.getKey() - 1, front.left));
}
if (front.right != null) {
// ④ 向右
queue.add(new SimpleImmutableEntry<>(entry.getKey() + 1, front.right));
}
}
return new ArrayList<>(map.values());
}
值得注意的是,二叉树中同一层同一列位置可能包含多个节点,如上图节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> 4 4 </math>4 / <math xmlns="http://www.w3.org/1998/Math/MathML"> 5 5 </math>5 和节点 <math xmlns="http://www.w3.org/1998/Math/MathML"> 8 8 </math>8 / <math xmlns="http://www.w3.org/1998/Math/MathML"> 9 9 </math>9 。
如上代码只是把同一列元素放到同一列表中了,但是对于同一列里面一行出现的多个元素并没有加以区分,假设现在我们要求二叉树的 仰视图 呢?
仰视图 即求二叉树每一列 最后一行 元素,如果最后一行 有多个呢?如上图第一列最后一行便有两个元素。
亦或,我们现在想同一列同一行的多个元素排序呢?
为了满足该条件,我们还需要再列基础上区分行,
java
// 三元组
// Apache commons 类库中提供 Triple 可直接使用
public class Triple<T, U, V> {
public final T first;
public final U second;
public final V third;
public Triple(T first, U second, V third) {
this.first = first;
this.second = second;
this.third = third;
}
}
public List<List<Integer>> verticalTraversal(TreeNode root) {
// 队列中要存储 "节点" / "节点所在列列号" / "节点所在行行号" 三个属性
Queue<Triple<Integer, Integer, TreeNode>> queue = new LinkedList<>();
// 列号 → 行号 → 同行同列下所有节点
TreeMap<Integer, TreeMap<Integer, List<Integer>>> map = new TreeMap<>();
if (root != null) {
// 根节点属于第 0 列, 第 1 行
queue.offer(new Triple<>(0, 1, root)); // ① 根节点入队
}
while (!queue.isEmpty()) {
Triple<Integer, Integer, TreeNode> triple = queue.poll();
TreeNode front = triple.third; // ② 访问
// 取出节点所属列
TreeMap<Integer, List<Integer>> column = map.computeIfAbsent(triple.first,
k -> new TreeMap<>());
// 取出节点所属行
List<Integer> list = column.computeIfAbsent(triple.second, k -> new ArrayList<>());
list.add(front.val); // 将节点放入对应行列中
if (front.left != null) {
// ③ 向左下
queue.add(new Triple<>(triple.first - 1, triple.second + 1, front.left));
}
if (front.right != null) {
// ④ 向右下
queue.add(new Triple<>(triple.first + 1, triple.second + 1, front.right));
}
}
return map.values().stream()
// 同行同列中多个元素排序, 需要时打开下行注释
// .peek(k -> k.values().forEach(Collections::sort))
// 将同列中所有行 list 打平成一个 list
.map(k -> k.values().stream().flatMap(r -> r.stream()).collect(Collectors.toList()))
.collect(Collectors.toList());
}
如上代码在 行 和 列 二维角度对二叉树中所有节点加以区分,这样如果要求 仰视图 只需要 对 map
进行如下操作,
java
return map.values().stream()
// 求每一列最后一行节点, 可能有多个
.map(k -> k.values().stream().reduce((f, s) -> s).orElse(new ArrayList<>()))
.collect(Collectors.toList());