树算法题的技巧总结

5 阅读6分钟

一、递归基础模板

二叉树的核心解题思路是递归。掌握递归的基本模式是解决所有二叉树问题的基础。

递归函数的基本结构

function dfs(node) {
    // 1. 终止条件(基本情况)
    if (!node) return;
    
    // 2. 处理当前节点(前序遍历位置)
    // do something...
    
    // 3. 递归处理左右子树
    dfs(node.left);
    dfs(node.right);
    
    // 4. 后续处理(后序遍历位置)
    // do something...
}

三种遍历方式

// 前序遍历:根 -> 左 -> 右
function preorder(node) {
    if (!node) return;
    console.log(node.val);  // 访问根
    preorder(node.left);    // 遍历左
    preorder(node.right);   // 遍历右
}

// 中序遍历:左 -> 根 -> 右
function inorder(node) {
    if (!node) return;
    inorder(node.left);     // 遍历左
    console.log(node.val);  // 访问根
    inorder(node.right);    // 遍历右
}

// 后序遍历:左 -> 右 -> 根
function postorder(node) {
    if (!node) return;
    postorder(node.left);   // 遍历左
    postorder(node.right);  // 遍历右
    console.log(node.val);  // 访问根
}

二、解题思维总结

1. 递归问题的思考框架

1. 确定递归函数的返回值和参数
   - 返回值:需要子树提供什么信息?
   - 参数:需要父节点传递什么信息?

2. 确定终止条件
   - 空节点怎么处理?

3. 确定单层递归逻辑
   - 当前节点做什么处理?
   - 如何调用递归函数?
   - 如何利用递归返回值?

2. 常见返回值设计

问题类型返回值设计
深度/高度number
是否满足条件boolean
需要多个信息[boolean, number] 或对象
路径/节点TreeNode 或数组

3. 时间复杂度与空间复杂度

  • 时间复杂度:通常为 O(n),每个节点访问一次
  • 空间复杂度
    • 递归栈空间:O(h),h 为树高,递归调用本身会使用系统栈来保存每一层调用的上下文,栈的深度等于递归的深度,即树的高度
    • 最坏情况(链状树):O(n)
    • 最好情况(平衡树):O(log n)

三、高频解题技巧

技巧一:自顶向下递归(Top-Down)

适用场景:需要从根节点向下传递信息(如边界值、路径等)

核心要点

  • 父节点通过参数向子节点传递信息
  • 每个节点根据传入的参数做判断

典型例题验证二叉搜索树

var isValidBST = function(root) {
    function dfs(node, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
        if (!node) return true;
        // 当前节点必须满足边界条件
        if (node.val <= min || node.val >= max) return false;
        // 递归左右子树,更新边界
        return dfs(node.left, min, node.val) && dfs(node.right, node.val, max);
    }
    return dfs(root);
};

技巧二:自底向上递归(Bottom-Up)

适用场景:需要从子树获取信息返回给父节点(如高度、直径、路径和等)

核心要点

  • 子树向父节点返回信息
  • 使用全局变量记录需要的结果
  • 返回值的定义要明确(如高度、是否平衡等)

典型例题1二叉树的最大深度

var maxDepth = function(root) {
    if (!root) return 0;
    const leftDepth = maxDepth(root.left);
    const rightDepth = maxDepth(root.right);
    return Math.max(leftDepth, rightDepth) + 1;
};

典型例题2二叉树的直径

var diameterOfBinaryTree = function(root) {
    let maxDiameter = 0;
    
    function getHeight(node) {
        if (!node) return 0;
        const left = getHeight(node.left);
        const right = getHeight(node.right);
        // 当前节点的直径 = 左子树高度 + 右子树高度
        maxDiameter = Math.max(maxDiameter, left + right);
        // 返回当前子树的高度
        return Math.max(left, right) + 1;
    }
    
    getHeight(root);
    return maxDiameter;
};

技巧三:返回多个值的递归

适用场景:需要同时返回多个信息(如是否平衡 + 高度)

核心要点

  • 使用数组或对象返回多个值
  • 递归函数同时承担计算和判断的职责

典型例题平衡二叉树

var isBalanced = function(root) {
    return check(root)[0];
};

function check(node) {
    if (!node) return [true, 0];  // [是否平衡, 高度]
    
    const [leftBalanced, leftHeight] = check(node.left);
    const [rightBalanced, rightHeight] = check(node.right);
    
    const balanced = leftBalanced && rightBalanced 
                     && Math.abs(leftHeight - rightHeight) <= 1;
    const height = Math.max(leftHeight, rightHeight) + 1;
    
    return [balanced, height];
}

技巧四:层序遍历(BFS)

适用场景:按层处理问题(如层序遍历、右视图等)

核心要点

  • 使用队列实现
  • 每次处理一层的所有节点
  • 通过 levelSize 控制每层的遍历次数

模板代码二叉树的层序遍历

var levelOrder = function(root) {
    if (!root) return [];
    
    const queue = [root];
    const result = [];
    
    while (queue.length > 0) {
        const levelSize = queue.length;  // 当前层的节点数
        const currentLevel = [];
        
        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            currentLevel.push(node.val);
            
            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }
        
        result.push(currentLevel);
    }
    
    return result;
};

典型例题二叉树的右视图

var rightSideView = function(root) {
    if (!root) return [];
    
    const queue = [root];
    const result = [];
    
    while (queue.length > 0) {
        const levelSize = queue.length;
        
        for (let i = 0; i < levelSize; i++) {
            const node = queue.shift();
            // 当前层的最后一个节点就是右视图节点
            if (i === levelSize - 1) {
                result.push(node.val);
            }
            if (node.left) queue.push(node.left);
            if (node.right) queue.push(node.right);
        }
    }
    
    return result;
};

技巧五:二叉搜索树(BST)特性利用

BST 核心性质

  • 左子树所有节点值 < 根节点值
  • 右子树所有节点值 > 根节点值
  • 中序遍历结果是有序的

核心要点

  • 利用 BST 的有序性简化问题
  • 中序遍历 = 升序遍历

典型例题1二叉搜索树的最近公共祖先

var lowestCommonAncestor = function(root, p, q) {
    // 两个节点都在左子树
    if (p.val < root.val && q.val < root.val) {
        return lowestCommonAncestor(root.left, p, q);
    }
    // 两个节点都在右子树
    if (p.val > root.val && q.val > root.val) {
        return lowestCommonAncestor(root.right, p, q);
    }
    // 当前节点就是最近公共祖先
    return root;
};

典型例题2二叉搜索树中第K小的元素

var kthSmallest = function(root, k) {
    let count = 0;
    let result = null;
    
    function inorder(node) {
        if (!node || result !== null) return;
        
        inorder(node.left);
        
        count++;
        if (count === k) {
            result = node.val;
            return;
        }
        
        inorder(node.right);
    }
    
    inorder(root);
    return result;
};

技巧六:构建父节点映射

适用场景:需要向上遍历(访问父节点)的情况

核心要点

  • 先用 DFS 构建父节点映射
  • 然后可以像图一样进行多方向遍历

典型例题二叉树中所有距离为 K 的结点

var distanceK = function(root, target, k) {
    const parentMap = new Map();
    
    // 第一步:构建父节点映射
    function buildParentMap(node, parent) {
        if (!node) return;
        parentMap.set(node, parent);
        buildParentMap(node.left, node);
        buildParentMap(node.right, node);
    }
    buildParentMap(root, null);
    
    // 第二步:从 target 开始 BFS
    const result = [];
    const visited = new Set();
    
    function find(node, distance) {
        if (!node || visited.has(node)) return;
        visited.add(node);
        
        if (distance === k) {
            result.push(node.val);
            return;
        }
        
        find(node.left, distance + 1);
        find(node.right, distance + 1);
        find(parentMap.get(node), distance + 1);  // 向上遍历
    }
    
    find(target, 0);
    return result;
};

技巧七:分治构建二叉树

适用场景:根据遍历序列重建二叉树

核心要点

  • 前序的第一个元素是根
  • 中序中根的位置划分左右子树
  • 用哈希表优化查找,避免 O(n) 的 indexOf

典型例题从前序与中序遍历序列构造二叉树

var buildTree = function(preorder, inorder) {
    // 用哈希表优化查找
    const inorderMap = new Map();
    for (let i = 0; i < inorder.length; i++) {
        inorderMap.set(inorder[i], i);
    }
    
    function build(preStart, inStart, inEnd) {
        if (inStart > inEnd) return null;
        
        const rootVal = preorder[preStart];
        const root = new TreeNode(rootVal);
        const index = inorderMap.get(rootVal);
        
        const leftSize = index - inStart;
        
        root.left = build(preStart + 1, inStart, index - 1);
        root.right = build(preStart + 1 + leftSize, index + 1, inEnd);
        
        return root;
    }
    
    return build(0, 0, inorder.length - 1);
};

技巧八:序列化与反序列化

典型例题二叉树的序列化与反序列化

// 序列化:前序遍历
var serialize = function(root) {
    if (!root) return 'null';
    return root.val + ',' + serialize(root.left) + ',' + serialize(root.right);
};

// 反序列化
var deserialize = function(data) {
    const values = data.split(',');
    
    function build() {
        const val = values.shift();
        if (val === 'null') return null;
        
        const node = new TreeNode(parseInt(val));
        node.left = build();
        node.right = build();
        return node;
    }
    
    return build();
};

四、易错点提醒

  1. 递归终止条件:不要忘记处理空节点
  2. 全局变量更新:在自底向上递归中,注意全局变量的更新时机
  3. 负数处理:路径和问题中,负数可以置为 0 表示不选
    const leftMax = Math.max(dfs(node.left), 0);
    
  4. BST 边界:验证 BST 时注意边界值要用 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER
  5. 索引计算:分治构建树时,注意索引的计算不要出错