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

二叉树

二叉树是一种基础的数据结构,它由节点(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],这正是按照先序遍历(根->左->右)的顺序访问节点所得到的结果。

结语

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

相关推荐
咸鱼翻面儿4 分钟前
Javascript异步,这次我真弄懂了!!!
javascript
brrdg_sefg4 分钟前
Rust 在前端基建中的使用
前端·rust·状态模式
m0_7482309429 分钟前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
qq_5895681037 分钟前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
游是水里的游38 分钟前
【算法day19】回溯:分割与子集问题
算法
不想当程序猿_38 分钟前
【蓝桥杯每日一题】分糖果——DFS
c++·算法·蓝桥杯·深度优先
南城花随雪。1 小时前
单片机:实现FFT快速傅里叶变换算法(附带源码)
单片机·嵌入式硬件·算法
dundunmm1 小时前
机器学习之scikit-learn(简称 sklearn)
python·算法·机器学习·scikit-learn·sklearn·分类算法
古希腊掌管学习的神1 小时前
[机器学习]sklearn入门指南(1)
人工智能·python·算法·机器学习·sklearn
波音彬要多做1 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法