图解树的遍历

先序、中序、后序遍历

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

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

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

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

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

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

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

为了实现二叉树的 垂直遍历 ,我们给二叉树每一列编号,根节点为第 0 0 0 列,根节点的左孩子为第 − 1 -1 −1 列,根节点的右孩子为第 1 1 1 列,以此类推,节点向左走列号 − 1 -1 −1, 向右走列号 + 1 +1 +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());
}

值得注意的是,二叉树中同一层同一列位置可能包含多个节点,如上图节点 4 4 4 / 5 5 5 和节点 8 8 8 / 9 9 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());
相关推荐
江畔柳前堤4 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore5 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考7 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX8 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面8 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号8 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽9 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI9 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者10 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者10 小时前
【征程6】校准量化中HistogramObserver解析
算法