94_二叉树的中序遍历(算法思路,(循环和递归的两种实现))

149 阅读4分钟

题目描述

给定一个二叉树的根节点 root ,返回 它的 中序 遍历

示例 1:

img

输入:root = [1,null,2,3]
输出:[1,3,2]

示例 2:

输入:root = []
输出:[]

示例 3:

输入:root = [1]
输出:[1]

思路

有一棵二叉树:

               中
              /  \
             左   右
  • 前序遍历:中,左,右
  • 中序遍历:左,中,右
  • 后序遍历:左,右,中

递归的方式相对比较简单,就是递归的遍历左子树,然后添加根节点的值,再去递归遍历右子树

下面有一种比较通用的方法,这种方法使用栈和循环来进行遍历,原文内容来自leetcode.cn/problems/bi…

核心思想如下:

  • 使用颜色标记节点的状态,新节点为白色,已访问的节点为灰色。
  • 如果遇到的节点为白色,则将其标记为灰色,然后将其右子节点、自身、左子节点依次入栈。
  • 如果遇到的节点为灰色,则将节点的值输出。

下面谈一些我的理解:

颜色标记法通过为每个节点分配颜色来跟踪其访问状态:

  • WHITE (0):节点尚未被处理,需要遍历其子节点。
  • GRAY (1):节点已遍历完左子树,准备访问节点本身。
const WHITE = 0, GRAY = 1, res = [];
const stack = [[WHITE, root]];
  • 颜色常量
    • WHITE表示需要处理的节点。
    • GRAY 表示节点已经处理过左子树,待访问节点值。
  • 结果数组 res:用于存储中序遍历的结果。
  • 栈 stack:初始化时将根节点以 WHITE 标记压入栈中。栈用于模拟递归调用。

遍历过程:

while (stack.length > 0) {
    const [color, node] = stack.pop();
    if (!node) continue;
    if (color === WHITE) {
        stack.push([WHITE, node.right])//WHITE(0):节点尚未被处理,需要遍历其子节点。
        stack.push([GRAY, node])//GRAY(1):节点已遍历完左子树,准备访问节点本身
        stack.push([WHITE, node.left])//WHITE(0):节点尚未被处理,需要遍历其子节点。
    } else {
        res.push(node.val);
    }
}
  • 循环条件:当栈不为空时,持续执行遍历。
  • 弹出栈顶元素:每次从栈中弹出一个 [颜色, 节点] 的元组。
  • 处理空节点:如果当前节点为 null,则跳过本次循环。
  • 处理 WHITE 节点
    • 右子节点:将右子节点以 WHITE 标记压入栈中,稍后需要遍历右子树。
    • 当前节点:将当前节点以 GRAY 标记压入栈中,表示左子树已遍历完,待访问节点值。
    • 左子节点:将左子节点以 WHITE 标记压入栈中,优先遍历左子树。
  • 处理 GRAY 节点:将节点的值添加到结果数组 res 中。

先序遍历是左中右,为什么添加到栈的顺序是右中左:

栈是后进先出,所以我们通过右中左的添加顺序来构造我们想要的出栈顺序,实际上的出栈顺序是左中右

同理,对于前序或者后续遍历,我们只要调整入栈顺序即可

修改入栈顺序实现不同遍历

遍历方式入栈顺序(从上到下)
中序遍历右子节点 [WHITE] -> 当前节点 ([GRAY]) -> 左子节点 ([WHITE])
前序遍历右子节点 ([WHITE] -> 左子节点 ([WHITE]) -> 根节点 ([GRAY])
后序遍历根节点 ([GRAY]) -> 右子节点 ([GRAY]) -> 左子节点 ([GRAY])

复杂度分析

时间复杂度

所有三种遍历方法的时间复杂度均为 O(n),其中 n 是二叉树的节点数。

空间复杂度

空间复杂度主要取决于栈的大小,最坏情况下为 O(n)。

code

递归

var inorderTraversal = function (root, res = []) {
    if(!root) return res
    inorderTraversal(root.left, res)//递归遍历左子树
    res.push(root.val)//添加子树的根节点
    inorderTraversal(root.right, res)//递归遍历右子树
    return res
};

循环

/**
 * 定义一个二叉树
 */
function TreeNode(val, left = null, right = null) {
    this.val = val === undefined ? 0 : val;
    this.left = left;
    this.right = right;
}

/**
 * 中序遍历二叉树
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function (root) {
    const WHITE = 0,
        GRAY = 1,
        res = [];
    const stack = [[WHITE, root]];

    while (stack.length > 0) {
        const [color, node] = stack.pop();
        if (!node) continue;
        if (color === WHITE) {
            stack.push([WHITE, node.right])//WHITE(0):节点尚未被处理,需要遍历其子节点。
            stack.push([GRAY, node])//GRAY(1):节点已遍历完左子树,准备访问节点本身
            stack.push([WHITE, node.left])//WHITE(0):节点尚未被处理,需要遍历其子节点。
        } else {
            res.push(node.val);
        }
    }
    return res;
};
// 示例输入:构建二叉树 [1, null, 2, 3]
/*
    1
     \
      2
     /
    3
*/
const root = new TreeNode(1);
root.right = new TreeNode(2);
root.right.left = new TreeNode(3);

// 调用中序遍历函数
const result = inorderTraversal(root);
console.log(result); // 输出: [1, 3, 2]