二叉树遍历全解析:递归与迭代实现

二叉树

二叉树是一种基础的数据结构,它由节点(Nodes)组成,每个节点最多有两个子节点,通常被称为左子节点(left child)和右子节点(right child),其中空树也可以称为二叉树。

二叉树的基本概念

  • 根节点(Root):位于树的顶部,没有父节点
  • 叶子节点(Leaf):没有子节点的节点。
  • 度(Degree):一个节点的子节点数量,二叉树中节点的最大度为2。如下图所示,右子节点3的度为1,叶子节点6的度为0。
  • 边(Edge):连接父子节点的线。
  • 路径(Path):从一个节点到另一个节点所经过的边的序列。
  • 深度(Depth):从根节点到某个节点的路径上边的数量。如果根节点的深度定义为0,那么下图的二叉树的深度为2。
  • 高度(Height):树中任意节点的最大深度,整棵树的高度是根节点的高度。下图的高度为3。

二叉树的类型

  • 二叉搜索树(Binary Search Tree, BST):每个节点的值都大于等于其左子树中所有节点的值,并且小于等于其右子树中所有节点的值。
  • 满二叉树(Full Binary Tree):所有层次的节点数都是最大节点数,即每个节点都有两个子节点,除了叶子节点。
  • 完全二叉树(Complete Binary Tree):除了最后一层外,每一层的节点都被完全填充,且最后一层的节点都尽可能地靠左排列。
  • 平衡二叉树(Balanced Binary Tree):左右两个子树的高度差不超过1,比如AVL树和红黑树都是平衡二叉树的典型代表。

二叉树常用于实现动态查找表、堆排序、表达式求值等领域,它的遍历算法(前序遍历、中序遍历、后序遍历和层序遍历)也是数据结构中的基本知识点。

二叉树的遍历

接下来都用这张图片来介绍一下二叉树的遍历方法,遍历二叉树的方法有很多种,今天就介绍四种方法,先序遍历、中序遍历、后序遍历、迭代方法(用栈和数组实现)。

用代码来实现这个二叉树,在这个例子中,root 是树的根节点,它的值为 1。从根节点出发,二叉树分为两部分:左子树和右子树。每个节点包含一个值(val)以及指向其左子节点(left)和右子节点(right)的引用。未提及的子节点默认为 null 或不存在。

js 复制代码
const root = {
    val:1,
    left: {
        val: 2,
        left: {
            val: 4
        },
        right: {
            val: 5
        }
    },
    right: {
        val: 3,
        right: {
            val: 6
        }
    }
}

方法一 先序遍历 --- 根左右(1 2 4 5 3 6)

先序遍历、中序遍历和后序遍历其实都是递归思维,在函数中再调用自身。先序遍历,就是按照根左右的顺序,每次都先遍历根节点,其次遍历左节点,最后遍历右节点,像上图用先序遍历的结果就是1 2 4 5 3 6

js 复制代码
// 定义先序遍历函数,接收一个参数 root,即当前正在访问的节点
function preorder(root) {
    // 如果当前节点为空(到达了叶子节点以下的位置),则直接返回,结束本次递归
    if (!root) return
    
    // 访问当前节点:打印当前节点的值
    console.log(root.val);

    // 递归遍历左子树:调用 preorder 函数,传入当前节点的左子节点作为新的根节点
    preorder(root.left)

    // 递归遍历右子树:调用 preorder 函数,传入当前节点的右子节点作为新的根节点
    preorder(root.right)
}

总结一下先序遍历用代码实现就是先访问当前节点,再递归遍历左子树,接着递归遍历右子树即可。打印结果如下所示。

方法二 中序遍历 --- 左根右(4 2 5 1 3 6)

中序遍历的顺序是:左子树-根节点-右子树。

js 复制代码
//中序遍历
function inorder(root) {
    if(!root) return

    inorder(root.left)
    console.log(root.val);
    inorder(root.right)
}

inorder(root)
  1. 递归出口 :首先检查 root 是否为空,如果为空,则直接返回,遍历结束。

  2. 递归遍历左子树inorder(root.left)。在访问当前节点之前,先递归地遍历它的左子树。这样可以确保所有左子树的节点在根节点之前被访问。

  3. 访问当前节点console.log(root.val);。一旦到达树的最左边节点,打印(或处理)该节点的值,然后回溯到父节点。

  4. 递归遍历右子树inorder(root.right)。在左子树和根节点都被访问过后,开始遍历右子树。这保证了所有右子树的节点在当前节点之后被访问。

最后输出结果如下所示。

方法三 后序遍历 --- 左右根(4 5 2 6 3 1)

后序遍历的顺序是左子树-右子树-根节点

js 复制代码
// 后序遍历
function lastorder(root) {
    if(!root) return

    lastorder(root.left)
    lastorder(root.right)
    console.log(root.val);
}
lastorder(root)

先序、中序和后序的代码基本一样,只是换了一下顺序,这里就简单介绍一下后序遍历,先找递归出口,root 是否为空直接返回。接着递归遍历左子树,再递归遍历右子树,当左右子树都已被访问之后,处理当前节点,打印其值。这确保了节点的访问顺序遵循"左右根"的后序遍历规则。

打印结果如下所示。

方法四 迭代方法(用栈和数组实现)

用迭代方法实现先序遍历

在执行前序遍历的过程中,遵循以下步骤:

  1. 初始化 : 将根节点root压入栈中,作为遍历的起点。

  2. 循环处理:

    • 弹出并访问 : 开始循环,每次从栈顶弹出一个节点赋值给cur,并将cur的值加入结果数组res,表示当前节点已被访问。
    • 压入子节点 :
      1. 先右后左 : 首先检查cur的右子节点是否存在,若存在,则将其压入栈中。这一操作基于栈的后进先出特性,右子节点先入栈,那么左子节点先出栈,符合先序遍历"根左右"的规则。
      2. 然后检查cur的左子节点,若存在,同样将其压入栈中。左子节点虽然后压入,但由于栈的机制,它会在下一个循环中先于右子节点被访问。
    • 这个过程持续进行,直到栈变空,意味着所有的节点都已按先序遍历的顺序被访问并加入结果数组res
js 复制代码
var preorderTraversal = function(root) {
    if (!root) return []
    const res = []
    const stack = []
    stack.push(root)

    while (stack.length) {
        const cur = stack.pop()
        res.push(cur.val)
        if (cur.right) {
            stack.push(cur.right)
        }
        if (cur.left) {
            stack.push(cur.left)
        }
    }
    
    return res
};
console.log(preorderTraversal(root));

初始化变量

  • res:一个空数组,用于存储遍历结果。
  • stack:一个空数组,作为栈来辅助迭代过程,存储待访问的节点。
  • cur:当前正在访问的节点,初始化为根节点 root

执行步骤

针对这棵特定的二叉树,我们可以按照迭代方法的遍历逻辑来逐步分析其先序遍历的过程。二叉树结构如下:

markdown 复制代码
    1
   / \
  2   3
 / \   \
4   5   6

遍历步骤

  1. 初始化 : 根节点1入栈。

  2. 第一次循环:

    • 弹出并访问 : 弹出1,加入res,得到res = [1]
    • 压入子节点 : 先压入右子节点3,再压入左子节点2,栈状态变为[3, 2]
  3. 第二次循环:

    • 弹出并访问 : 弹出2,加入res,得到res = [1, 2]
    • 压入子节点 : 先压入右子节点5,再压入左子节点4,栈状态变为[3, 5, 4]
  4. 第三次循环:

    • 弹出并访问 : 弹出4,加入res,得到res = [1, 2, 4]
    • 无子节点 : 4无子节点,直接进入下一次循环。
  5. 第四次循环:

    • 弹出并访问 : 弹出5,加入res,得到res = [1, 2, 4, 5]
    • 无子节点 : 5无子节点,继续。
  6. 第五次循环:

    • 弹出并访问 : 弹出3,加入res,得到res = [1, 2, 4, 5, 3]
    • 压入子节点 : 3只有右子节点6,压入栈中,栈状态变为[6]
  7. 第六次循环:

    • 弹出并访问 : 弹出6,加入res,得到res = [1, 2, 4, 5, 3, 6]
    • 无子节点 : 6无子节点,栈变空。

至此,所有节点均被访问,栈也为空,先序遍历结束。最终的遍历结果数组res[1, 2, 4, 5, 3, 6],这正是按照先序遍历(根->左->右)的顺序访问节点所得到的结果。

结语

在这篇文章只介绍了先序遍历的迭代方法,那你知道如何用迭代方法来实现中序遍历和后序遍历吗?可以评论区告诉我。

相关推荐
辻戋1 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保1 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun2 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp2 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.3 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
电鱼智能的电小鱼4 小时前
基于电鱼 AI 工控机的智慧工地视频智能分析方案——边缘端AI检测,实现无人值守下的实时安全预警
网络·人工智能·嵌入式硬件·算法·安全·音视频
孫治AllenSun5 小时前
【算法】图相关算法和递归
windows·python·算法
TeleostNaCl5 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
格图素书6 小时前
数学建模算法案例精讲500篇-【数学建模】DBSCAN聚类算法
算法·数据挖掘·聚类
前端大卫7 小时前
为什么 React 中的 key 不能用索引?
前端