揭秘二叉树遍历:从基础到实战 🌳

99 阅读5分钟

二叉树的遍历是怎么实现的?

13.jpg

在数据结构的世界里,二叉树以其独特的分支形态承载着复杂的数据关系。对于一棵二叉树而言,遍历是一种不可或缺的操作,它让我们能够按照特定顺序访问树中的每一个节点。面对这样的一棵树(如下图所示),你可能会想,如何才能系统地访问其中所有的元素呢?

image.png

当你站在树的根部,想要探索每一片叶子的秘密时,有几种经典的方法可以帮助你达成目标:先序遍历、中序遍历 和 后序遍历,这是我们第一时间就可以想到的方法,但其实还有一种解法,那就是层序遍历(特别适合广度优先搜索),而接下来,我们就将使用代码来完成我们想到的这几种方案。

树的概念与递归的关系?

递归是计算机科学中一种强大的工具,函数可以通过直接或间接的方式调用自身来解决问题。在遍历二叉树时,递归提供了一种简洁且易于理解的方法。每次递归调用实际上都是对问题的一次细分,直到达到一个简单的基础情况(base case),即遇到空树时停止。这种自顶向下的过程可以很好地模拟人类思维,使复杂的任务变得简单易懂。

树的概念本质上是递归的

树的概念本身就是基于递归的思想构建的。一棵二叉树可以被看作是由根节点以及两个子树构成,而每个子树本身也是一棵二叉树。这种自相似性使得递归成为处理树结构的理想方法。通过递归,我们可以将树的遍历问题分解为几个基本步骤:

  1. 退出条件:当遇到空树时,即root === null,递归应立即返回,不再继续。
  2. 递归公式:根据不同的遍历顺序(如先序、中序、后序),定义处理当前节点及左右子树的具体步骤。
  3. 函数调用自己:递归的核心在于函数可以直接或间接调用自己,逐层深入树的每一个分支,直至触及最底层的叶子节点。

方案的代码实现

14.jpg

二叉树的建立

二叉树是一种每个节点最多有两个子节点的数据结构,这两个子节点通常被区分为左子节点和右子节点。而空树则是没有根节点的特殊情形。基于此概念,我们构建了一棵如图所示的二叉树,其结构简单明了,便于演示不同遍历方法的具体实现。

const root = {
    val: 'A',
    left: {
        val: 'B',
        left: {
            val: 'D'
        },
        right: {
            val: 'E'
        }
    },
    right: {
        val: 'C',
        right: {
            val: 'F'
        }
    }
}

image.png

由此我们就实现了第一步,成功创建了一棵树,为接下来的遍历操作奠定了基础。

先序遍历的实现

先序遍历遵循“根-左-右”的访问顺序,即先访问根节点,然后依次访问左子树和右子树。这种遍历方式有助于复制或重建二叉树,因为它能确保父节点总是出现在其子节点之前。

function preorder(root) {
    //退出条件
    //空树
    if (!root) return;
    // 处理当前节点
    console.log(root.val);
    // 递归访问左子树
    preorder(root.left);
    // 递归访问右子树
    preorder(root.right);
}

preorder(root);

中序遍历的实现

中序遍历采用“左-根-右”的顺序进行访问。对于二叉搜索树来说,中序遍历可以得到一个按升序排列的节点序列,因此常用于需要排序输出的场景。

function inOrder(root) {
    if (!root) {
        return
    }
    inOrder(root.left)
    console.log(root.val)
    inOrder(root.right)
}
inOrder(root);

后序遍历的实现

后序遍历“左-右-根”的访问模式意味着直到处理完所有子节点之后才会处理根节点。适用于需要在处理完所有子节点后再处理根节点的情况,例如删除二叉树中的节点时。

function postOrder(root) {
    if (!root) {
        return
    }
    postOrder(root.left)
    postOrder(root.right)
    console.log(root.val)
}
postOrder(root);

层序遍历的实现

层序遍历按照从上到下、同一层从左至右的顺序逐层访问。这种方法特别适合于广度优先搜索(BFS),并且可以很容易地找到最短路径问题的答案。为了实现层序遍历,我们引入队列(先进先出,FIFO)来辅助操作,这样可以避免因深度过大而导致的栈溢出问题。

//层序遍历
class TreeNode {
    constructor(val) {
        this.val = val;
        this.left = this.right = null;
    }
}
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.left = new TreeNode(6);
console.log(root);
function levelOrderTraversal(root) {
    if (!root) {
        return [];
    }
    const result = [];
    //根节点入队
    //借助队列   先进先出 FIFO   栈 LIFO
    const queue = [root];
    while (queue.length) {
    //队头出队
        const constructor = queue.shift(); 
        // 处理当前节点
        result.push(constructor.val);
        //将左子节点加入队尾
        if (constructor.left !== null) {
            queue.push(constructor.left);
        }
        //将右子节点加入队尾
        if (constructor.right !== null) {
            queue.push(constructor.right);
        }
    }
    return result;
}
console.log(levelOrderTraversal(root));

这段代码首先定义了一个TreeNode类用于创建二叉树节点,每个节点包含一个值val和指向左右子节点的引用。接着构建了一棵简单的二叉树,并通过levelOrderTraversal函数进行层序遍历。该函数使用队列(先进先出FIFO)来追踪待访问的节点:初始时将根节点加入队列,然后进入循环,每次从队列中取出一个节点处理(这里是记录节点值),再依次将其左右子节点(如果存在)加入队列尾部,直到队列为空,最终返回的结果数组包含了按层序访问的节点值列表,这就是本次层序遍历的全过程啦!

结语

希望这篇文章能激发你对数据结构及算法的兴趣,鼓励你进一步探索这个充满惊喜与乐趣的领域。每一次遍历都是一场独特的冒险,愿你在编程的旅途中发现属于自己的宝藏和乐趣!😊