关于二叉树的遍历方法及相关例题

456 阅读11分钟

前言

在这个数字化时代,树状结构以其高效的组织能力在计算机科学中扮演着关键角色。本文将深入探讨二叉树及其遍历方法,为理解和应用这一基础数据结构提供指导。

正文

树状结构

树状结构是一种层次化的数据组织方式,它由节点组成,每个节点有零个或多个子节点,但只有一个父节点(除了根节点以外)。 image.png

树状结构通常使用对象来表示,其中每个节点可以是一个对象,具有以下属性:

  • 值(Value):节点存储的数据。
  • 子节点(Children):一个子节点数组或列表,包含该节点的所有子节点。
  • 层次(Level) :从根节点到某一节点的路径上的节点数。
  • 高度(Height) :树中最长路径的层次数。
  • 度(Degree) :一个节点的子节点数量。对于叶子节点,度为0。

二叉树

  • 满二叉树(Full Binary Tree)
  1. 完全填满的层:除了最后一层之外,满二叉树的所有层都被完全填满。这意味着除了最后一层,每一层的所有节点都有两个子节点。
  2. 最后一层的填充:在满二叉树中,最后一层的节点尽可能地集中在左侧。也就是说,如果最后一层不能被完全填满,那么所有存在的节点都应该位于左侧,从左到右排列。
  3. 高度:满二叉树的高度是其节点数的对数(以2为底)。例如,一个有7个节点的满二叉树的高度是3,因为23=823=8,而7小于8且最接近8的2的幂是8。
  4. 节点数:满二叉树的节点数是其高度的指数。如果一个满二叉树的高度是ℎh,那么它最多可以有2ℎ−12h−1个节点。
  5. 形状:满二叉树的形状是对称的,左边的子树和右边的子树具有相同的高度,并且都是满二叉树。
  6. 性质:满二叉树是二叉树中的一种特殊情况,它具有一些有用的性质,例如,可以通过节点的层和位置来快速计算节点的索引,或者通过索引来快速定位节点的父节点和子节点。

image.png

遍历方法

关于遍历方法,笔者在这里推荐这篇文章,作者采用了动画的形式帮助我们理解:

数据结构——二叉树先序、中序、后序及层次四种遍历(C语言版)_中序遍历-CSDN博客

先序遍历(Preorder Traversal)

-   访问根节点。
-   递归地先序遍历左子树。
-   递归地先序遍历右子树。
秒解

前序遍历可以想象为,一个小人从一棵二叉树根节点为起点,沿着二叉树外沿,逆时针走一圈回到根节点,路上遇到的元素顺序,就是先序遍历的结果 image.png 先序遍历结果为:A B D H I E J C F K G

实现代码
function preorder(root) {
  if (!root) {
    return
  }
  console.log(root.val);
  preorder(root.left)
  preorder(root.right)
}

preorder(root)

image.png

中序遍历(Inorder Traversal)

-   递归地中序遍历左子树。
-   访问根节点。
-   递归地中序遍历右子树。
秒解

中序遍历可以看成,二叉树每个节点,垂直方向投影下来(可以理解为每个节点从最左边开始垂直掉到地上),然后从左往右数,得出的结果便是中序遍历的结果

image.png 中遍历结果为:H D I B E J A F K C G

实现代码
function inorder(root) {
  if (!root) {
    return
  }
  inorder(root.left)
  console.log(root.val)
  inorder(root.right)
}
inorder(root)

image.png

后序遍历(Postorder Traversal)

-   递归地后序遍历左子树。
-   递归地后序遍历右子树。
-   访问根节点。
秒解

后序遍历就像是剪葡萄,我们要把一串葡萄剪成一颗一颗的。

就是围着树的外围绕一圈,如果发现一剪刀就能剪下的葡萄(必须是一颗葡萄)(也就是葡萄要一个一个掉下来,不能一口气掉超过1个这样),就把它剪下来,组成的就是后序遍历了。

后序遍历中,根节点默认最后面

image.png

实现代码
function postorder(root) {
  if (!root) {
    return
  }
  postorder(root.left)
  postorder(root.right)
  console.log(root.val)

}
postorder(root)

image.png

例题

先序遍历

力扣第144题: leetcode.cn/problems/bi…

题解

  • 思路:
  1. 检查空树:首先看看给的树(用root表示)是不是空的。如果是空的,就直接返回一个空的列表,因为空树里啥也没有。
  2. 准备工具:准备两个工具,一个是res,用来存放我们访问过的树节点的值,最后要返回的就是这个;另一个是stack,一个模拟的栈,用来帮助我们记住接下来要访问哪些节点。
  3. 开始遍历:把树的根节点放进栈里,然后开始一个循环,只要栈里还有节点,就继续。
  4. 访问节点:在循环里,我们从栈里拿出一个节点(称为cur),然后把这个节点的值加到res列表的末尾。这就像是我们先看到了树的根。
  5. 处理子节点:接着,我们把cur节点的右孩子(如果有的话)放进栈里,然后再把左孩子放进栈里。这样做是因为我们想先访问左孩子,再访问右孩子,但因为栈是后进先出的,所以后放的节点会先被拿出来。
  6. 重复过程:循环继续,每次都是拿出一个节点,访问它,然后按顺序把孩子们放进栈里。
  7. 结束遍历:当栈空了,说明我们已经访问了所有的节点,可以停止了。
  8. 返回结果:最后,我们返回res列表,这个列表里按顺序存放了我们访问过的节点的值,这就是前序遍历的结果。
  • 代码:
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; // 返回遍历结果
};

中序遍历

力扣第94题: leetcode.cn/problems/bi…

题解

  • 思路:
  1. 检查空树:首先检查二叉树的根节点root是否为空。如果为空,直接返回空数组,因为空树的遍历结果是空的。
  2. 准备工具:定义一个数组res用于存储遍历过程中访问到的节点值。定义一个栈stack用于辅助遍历,模拟递归过程中的函数调用栈。
  3. 设置当前节点:设置一个指针cur指向根节点,作为遍历的起始点。
  4. 遍历循环:使用一个while循环,循环条件是栈非空或cur非空。这个循环确保了整个树的所有节点都能被访问。
  5. 向左深入:在循环内部,首先执行一个内层循环,将cur指针沿着左子树一直向下移动,直到找到最左侧的节点。在此过程中,将遇到的所有节点依次压入栈中。
  6. 访问节点:内层循环结束后,cur指向当前子树的最左侧节点。此时从栈中弹出一个节点,并将该节点的值添加到结果数组res中。
  7. 转向右子树:将cur指针移动到弹出节点的右子节点,准备开始遍历右子树。
  8. 继续或结束遍历:如果弹出的节点有右子树,则外层循环继续,cur沿着右子树向下移动;如果没有右子树,则继续执行外层循环,直到栈空且cur为空,此时说明所有节点已经被访问完毕。
  9. 返回结果:遍历完成后,返回存储了中序遍历节点值的结果数组res
  • 代码:
var inorderTraversal = function(root) {
    if (!root) {
        return []; // 如果根节点为空,返回空数组
    }
    const res = []; // 存储遍历结果的数组
    const stack = []; // 辅助栈
    let cur = root; // 当前节点指针

    // 循环直到当前节点为空或栈为空
    while (stack.length || cur) {
        // 遍历到当前节点的左子树底部
        while (cur) {
            stack.push(cur); // 将当前节点压入栈
            cur = cur.left; // 移动到左子节点
        }

        // 弹出栈顶节点并访问它,然后移动到右子树
        cur = stack.pop();
        res.push(cur.val); // 访问节点并添加到结果数组
        cur = cur.right; // 移动到右子节点
    }

    return res; // 返回中序遍历的结果
};

中序遍历比较复杂,这里画了 帮助理解的草图:

第一次,

  1. 根节点有值(1),进入while并入栈,
  2. 根节点的左节点(2)有值,进入while并入栈,
  3. 根节点左节点的左节点有值(4),进入while并入栈 e1f81f1580a7068616bd289d25a899e.jpg

第二次,

  1. 4的左节点为空,结束while,弹出4 d1c015fe4c0f09467fd0e90fd36c069.jpg

第三次,

  1. cur移动到4的右节点,
  2. 没有值,
  3. 直接跳过while,弹出2

1e33bb963e4fbaacdba5e2c42d38117.jpg

第四次,

  1. 移动到2的右节点(5),
  2. 进入while并入栈,
  3. 5左子节点为空,跳出while,
  4. 弹出5

1a6c99d5dcc1d81ce0d055622aa9f1a.jpg

第五次,

  1. 移动到5的右子节点,为空,
  2. cur没值,跳过while
  3. 弹出栈顶元素,此时为1,就弹出1

cf59cbb2e6623dc54428562ea9b471b.jpg

第六次,

  1. cur移到1的右子节点3,
  2. cur有值进入while并把3入栈,
  3. cur移到3的左子节点6,
  4. cur还是有值,进入while并把6入栈,
  5. 移到6的左节点,为空,跳出while
  6. 弹出栈顶元素,此时为6,弹出6

367f8ccaf2fab97a4ff938cc7891062.jpg

第七次,

  1. 移到6的右子节点,为空,
  2. cur为空,跳过while,
  3. 弹出栈顶元素3

2b46a38fc772d860fe172d4207b9f55.jpg

第八次,

  1. 移到3的右子节点7,
  2. cur有值,进入while并把cur值(7)入栈
  3. 移到7右子节点,没有,为空,结束while
  4. 弹出栈顶元素7

6d92554df10002d2700dd46af92070f.jpg

第九次,

  1. 移到7的右节点,为空,此时的stack长度为0,结束了外部的while, 返回 res(4 2 5 1 6 3 7)

后序遍历

力扣第145题: leetcode.cn/problems/bi…

题解

  1. 初始化检查:首先检查传入的根节点root是否为空。如果为空,直接返回空数组,因为空树的遍历结果是空的。
  2. 定义存储结构:定义一个数组res用于存储遍历过程中访问到的节点值。使用unshift方法将节点值插入到数组的开头,这样可以保证最终结果数组中的节点顺序是正确的后序遍历顺序。
  3. 初始化栈:定义一个栈stack,使用数组来模拟。初始化时将根节点root入栈。
  4. 遍历循环:使用while循环,循环条件是栈stack非空。这个循环确保了整个树的所有节点都能被访问。
  5. 弹出节点:在循环内部,弹出栈顶元素cur,这是当前需要访问的节点。
  6. 插入结果数组:将弹出的节点值cur.val插入到结果数组res的开头。这样做是因为后序遍历先访问子节点,最后访问根节点,所以先弹出的节点应放在数组的前面。
  7. 压栈子节点:在弹出当前节点后,先压入右子节点(如果存在),再压入左子节点(如果存在)。这样做是因为栈是后进先出(LIFO)的数据结构,先压入的节点后弹出,这样可以保证左子节点先被弹出访问。
  8. 继续遍历:循环继续,直到栈空,此时说明所有节点已经被访问完毕。
  9. 返回结果:遍历完成后,返回存储了后序遍历节点值的结果数组res
var postorderTraversal = function(root) {
    if (!root) return []; // 如果根节点为空,直接返回空数组
    const res = []; // 结果数组,存储遍历的节点值
    const stack = [root]; // 使用数组模拟栈,初始化时将根节点入栈

    while (stack.length) {
        const cur = stack.pop(); // 弹出栈顶元素
        res.unshift(cur.val); // 将弹出元素的值插入到结果数组的开头
        // 由于是后序遍历,先压右子节点再压左子节点,
        // 这样能保证左子节点先被弹出,然后是右子节点
        if (cur.left) stack.push(cur.left);
        if (cur.right) stack.push(cur.right);
    }

    return res; // 返回遍历结果
};

结语

以上就是本文全部内容,感谢您的阅读,希望本文对我们了解二叉树及其遍历方法有些许帮助。