图解树的遍历

先序、中序、后序遍历

我们可以将二叉树分成三个部分 ------ 根节点左右子树 ,其中左右子树分别又是二叉树。

二叉树的遍历是要以某种顺序依次访问二叉树中所有节点,根据访问这三部分顺序不同产生了不同遍历方式。

由于二叉树是有序树,所以访问左右子树相对顺序通常是不变的,即左子树总在右子树之前访问。

访问顺序能变的就只有根节点了,如果根节点在左右子树之前访问,那么我们称这种遍历方式为 先序遍历 (根左右),如果根节点在左右子树之间访问,就为 中序遍历 (左根右),如果根节点在左右子树之后访问,就为 后序遍历 (左右根)。

有时候,先序遍历也叫先根遍历 / 先序周游,中序遍历和后序遍历同理。

另外,由于二叉树是 递归 定义的,所以也要以相同顺序递归访问左右子树时。

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());
相关推荐
cwj&xyp23 分钟前
Python(二)str、list、tuple、dict、set
前端·python·算法
xiaoshiguang35 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡5 小时前
【C语言】判断回文
c语言·学习·算法
别NULL5 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇5 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
yuanbenshidiaos6 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习6 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA7 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo7 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc7 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法