二叉树遍历完全指南:从原理到JavaScript实战

128 阅读5分钟

引言

二叉树是数据结构中的基石,而遍历则是操作二叉树的基础。无论是前端框架中的虚拟DOM diff算法,还是后端数据库的索引结构,都离不开二叉树遍历的身影。本文将带你全面掌握二叉树的各种遍历方法,从递归到迭代,从深度优先到广度优先,结合实战代码示例,让你彻底搞懂二叉树遍历的精髓。

一、二叉树基础概念

1.1 什么是二叉树

二叉树是一种每个节点最多有两个子节点的树形结构,通常分为左子节点和右子节点。常见的二叉树类型包括满二叉树、完全二叉树、二叉搜索树等。

1.2 为什么需要遍历

遍历二叉树就是按某种顺序访问树中的所有节点,使每个节点被访问一次且仅被访问一次。遍历是许多二叉树操作的基础,如插入、删除、查找、排序等。

二、深度优先遍历(DFS)

深度优先遍历是沿着树的深度优先访问节点,分为前序、中序和后序三种方式,主要使用递归或栈实现。

2.1 前序遍历

遍历顺序:根节点 -> 左子树 -> 右子树

递归实现

var preorderTraversal = function(root) { // 二叉树 前序遍历 递归
    let res = []
    const preorder = function(cur) {
        if (!cur) return
        res.push(cur.val) // 访问根节点
        preorder(cur.left) // 递归左子树
        preorder(cur.right) // 递归右子树
    }
    preorder(root)  
    return res
};

迭代实现

利用栈先进后出的特性,先压右子节点再压左子节点:

var preorderTraversal = function(root) { // 二叉树 前序遍历 迭代法
    let res = []
    let stack = []
    if (!root) return res
    stack.push(root)
    while (stack.length) {
        let cur = stack.pop()
        res.push(cur.val) // 访问根节点
        if (cur.right) stack.push(cur.right) // 右子节点入栈
        if (cur.left) stack.push(cur.left) // 左子节点入栈
    }
    return res
};

2.2 中序遍历

遍历顺序:左子树 -> 根节点 -> 右子树

递归实现

var inorderTraversal = function(root) { // 二叉树 中序遍历 递归
    let res = []
    const dfs = function(cur) {
        if (!cur) return
        dfs(cur.left) // 递归左子树
        res.push(cur.val) // 访问根节点
        dfs(cur.right) // 递归右子树
    }
    dfs(root)
    return res
};

迭代实现

需要先遍历到最左节点,再回溯访问根节点和右子树:

var inorderTraversal = function(root) { // 二叉树 中序遍历 迭代法
    let res = []
    let stack = []
    let cur = root
    while (cur || stack.length) {
        if (cur) {
            stack.push(cur)
            cur = cur.left
        } else {
            cur = stack.pop()
            res.push(cur.val) // 访问根节点
            cur = cur.right
        }
    }
    return res
};

2.3 后序遍历

遍历顺序:左子树 -> 右子树 -> 根节点

递归实现

var postorderTraversal = function(root) { // 二叉树 后序遍历 递归
    let res = []
    const dfs = function(cur) {
        if (!cur) return
        dfs(cur.left) // 递归左子树
        dfs(cur.right) // 递归右子树
        res.push(cur.val) // 访问根节点
    }
    dfs(root)
    return res
};

迭代实现

可以通过前序遍历变种(根->右->左)再反转结果得到:

var postorderTraversal = function(root) { // 二叉树 后序遍历 迭代法
    let res = []
    let stack = []
    if (!root) return res
    stack.push(root)
    while (stack.length) {
        let cur = stack.pop()
        res.unshift(cur.val) // 结果头部插入
        if (cur.left) stack.push(cur.left)
        if (cur.right) stack.push(cur.right)
    }
    return res
};

2.4 统一迭代法

通过标记法(null节点)统一三种遍历的代码风格:

var preorderTraversal = function(root) { // 二叉树 前序遍历 统一迭代法
    let res = []
    let stack = []
    if (root) stack.push(root)
    while (stack.length) {
        let cur = stack.pop()
        if (!cur) {
            res.push(stack.pop().val)
            continue
        }
        if (cur.right) stack.push(cur.right)
        if (cur.left) stack.push(cur.left)
        stack.push(cur)
        stack.push(null) // 标记节点
    }
    return res
};

三、广度优先遍历(BFS)

广度优先遍历(层序遍历)是从根节点开始,逐层访问树的节点,主要使用队列实现。

3.1 基本层序遍历

var levelOrder = function(root) {
    let res = []  // 存储最终结果的二维数组
    let queue = [] // 使用队列辅助BFS遍历
    if (root) queue.push(root)
    while (queue.length) {
        let len = queue.length  // 当前层节点数
        let temp = []  // 临时存储当前层节点值
        while (len--) {
            let cur = queue.shift()
            temp.push(cur.val)
            if (cur.left) queue.push(cur.left)
            if (cur.right) queue.push(cur.right)
        }
        res.push(temp)
    }
    return res
};

3.2 层序遍历的应用

3.2.1 二叉树的最大深度

var maxDepth = function(root) { // 层次遍历
    let queue = []
    let depth = 0
    if (root) queue.push(root)  
    while (queue.length) {
        let len = queue.length
        while (len--) {
            let cur = queue.shift()
            if (cur.left) queue.push(cur.left)
            if (cur.right) queue.push(cur.right)
        }
        depth++ // 每遍历完一层,深度加1
    }   
    return depth
};

3.2.2 二叉树的最小深度

var minDepth = function(root) {
    let queue = []
    if (root) queue.push(root)
    let depth = 0
    while (queue.length) {
        let len = queue.length    
        while (len--) {
            let cur = queue.shift()
            // 找到第一个叶子节点,返回当前深度+1
            if (!cur.left && !cur.right) return depth + 1
            if (cur.left) queue.push(cur.left)
            if (cur.right) queue.push(cur.right)
        }
        depth++
    }
    return depth
};

3.2.3 每层节点的最大值

var largestValues = function(root) { // 层序遍历
    let res = []
    let queue = []
    if (root) queue.push(root)
    while(queue.length) {
        let len = queue.length
        let max = -Infinity // 初始化当前层最大值
        while (len--) {
            let cur = queue.shift()
            max = Math.max(max, cur.val) // 更新最大值
            if (cur.left) queue.push(cur.left)
            if (cur.right) queue.push(cur.right)
        }
        res.push(max)
    }
    return res
};

3.2.4 填充每个节点的下一个右侧节点指针

var connect = function(root) {
    let queue = []
    if (root) queue.push(root)
    while (queue.length) {
        let len = queue.length
        let pre = null
        while (len--) {
            let cur = queue.shift()
            if (pre) pre.next = cur // 连接同一层节点
            pre = cur
            if (cur.left) queue.push(cur.left)
            if (cur.right) queue.push(cur.right)
        }
    }
    return root
};

四、遍历方法对比与应用场景

遍历方法实现方式时间复杂度空间复杂度应用场景
前序遍历递归/迭代O(n)O(n)复制树、前缀表达式
中序遍历递归/迭代O(n)O(n)二叉搜索树排序
后序遍历递归/迭代O(n)O(n)删除节点、后缀表达式
层序遍历迭代(队列)O(n)O(n)树的深度/宽度、逐层处理

五、总结与拓展

二叉树遍历是数据结构中的基础操作,掌握各种遍历方法不仅能帮助我们更好地理解树形结构,也是解决复杂算法问题的基石。本文介绍的递归、迭代和统一迭代法各有特点:

  • 递归实现简洁易懂,但可能受限于栈深度
  • 迭代实现效率更高,适合大规模数据
  • 统一迭代法通过标记节点,实现了三种DFS遍历的代码统一

在实际开发中,我们需要根据具体问题选择合适的遍历方法。例如,在处理DOM树时常用DFS,而在处理层级关系明确的数据时常用BFS。