【每日算法】LeetCode124. 二叉树中的最大路径和

对前端开发者而言,学习算法绝非为了"炫技"。它是你从"页面构建者"迈向"复杂系统设计者"的关键阶梯。它将你的编码能力从"实现功能"提升到"设计优雅、高效解决方案"的层面。从现在开始,每天投入一小段时间,结合前端场景去理解和练习,你将会感受到自身技术视野和问题解决能力的质的飞跃。------ 算法:资深前端开发者的进阶引擎

LeetCode124. 二叉树中的最大路径和

1. 题目描述

1.1 问题定义

给定一个非空二叉树,返回其最大路径和。在这个问题中,路径被定义为一条从树中任意节点出发,沿着父子关系连接,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。

示例:

复制代码
输入:[-10,9,20,null,null,15,7]

   -10
   / \
  9  20
    /  \
   15   7

输出:42
解释:最优路径是 15 → 20 → 7,路径和为 15+20+7=42

2. 问题分析

2.1 问题本质

这是一个**树形DP(动态规划)**问题。对于二叉树中的任意节点,经过它的最大路径和可能由以下三部分组成:

  1. 该节点自身的值
  2. 左子树的最大贡献值(如果为正)
  3. 右子树的最大贡献值(如果为正)

2.2 难点分析

  • 路径可以不经过根节点:最大路径可能完全位于某个子树中
  • 负值节点的处理:负值节点可能不应该包含在最优路径中
  • 递归计算:需要设计正确的递归函数返回值和全局变量更新逻辑

2.3 前端视角

在前端开发中,这种问题类似于:

  • DOM树的某种属性最优路径计算
  • 组件树的最大渲染性能瓶颈分析
  • 组织架构树中的最大团队效能计算

3. 解题思路

3.1 核心思想

采用后序遍历-DFS的方式,自底向上计算:

  1. 对于每个节点,计算以该节点为"顶点"的最大路径和
  2. 向上返回的是从该节点向下的单边最大路径和(因为父节点只能选择左或右一边)
  3. 使用全局变量记录遍历过程中的最大路径和

3.2 最优解思路

分治递归法

  • 时间复杂度:O(n),每个节点访问一次
  • 空间复杂度:O(h),递归栈深度,h为树的高度
  • 这是最优解法,无法进一步优化时间复杂度

3.3 递归函数设计

javascript 复制代码
// 伪代码
function dfs(node):
    if node is null: return 0
    
    // 计算左右子树的最大贡献值(如果是负数则取0)
    leftGain = max(dfs(node.left), 0)
    rightGain = max(dfs(node.right), 0)
    
    // 计算以当前节点为顶点的最大路径和
    priceNewpath = node.val + leftGain + rightGain
    
    // 更新全局最大值
    maxSum = max(maxSum, priceNewpath)
    
    // 返回当前节点的最大贡献值(只能选择一边)
    return node.val + max(leftGain, rightGain)

4. 代码实现

4.1 JavaScript实现(最优解)

javascript 复制代码
/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */

/**
 * 最优解:DFS后序遍历
 * @param {TreeNode} root
 * @return {number}
 */
var maxPathSum = function(root) {
    let maxSum = -Infinity;
    
    /**
     * 递归计算最大贡献值
     * @param {TreeNode} node
     * @return {number} 返回以该节点为起点的最大路径和
     */
    const maxGain = function(node) {
        if (node === null) return 0;
        
        // 递归计算左右子树的最大贡献值
        const leftGain = Math.max(maxGain(node.left), 0);
        const rightGain = Math.max(maxGain(node.right), 0);
        
        // 当前节点作为路径顶点的最大路径和
        const currentPathSum = node.val + leftGain + rightGain;
        
        // 更新全局最大值
        maxSum = Math.max(maxSum, currentPathSum);
        
        // 返回当前节点的最大贡献值(只能选择一边)
        return node.val + Math.max(leftGain, rightGain);
    };
    
    maxGain(root);
    return maxSum;
};

4.2 TypeScript实现(类型安全版)

typescript 复制代码
class TreeNode {
    val: number;
    left: TreeNode | null;
    right: TreeNode | null;
    
    constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
        this.val = (val === undefined ? 0 : val);
        this.left = (left === undefined ? null : left);
        this.right = (right === undefined ? null : right);
    }
}

function maxPathSum(root: TreeNode | null): number {
    let maxSum = -Infinity;
    
    const dfs = (node: TreeNode | null): number => {
        if (!node) return 0;
        
        const leftGain = Math.max(dfs(node.left), 0);
        const rightGain = Math.max(dfs(node.right), 0);
        
        const currentPathSum = node.val + leftGain + rightGain;
        maxSum = Math.max(maxSum, currentPathSum);
        
        return node.val + Math.max(leftGain, rightGain);
    };
    
    dfs(root);
    return maxSum;
}

4.3 迭代法实现(使用栈模拟递归)

javascript 复制代码
/**
 * 迭代法实现:使用栈模拟后序遍历
 * 虽然空间复杂度相同,但代码更复杂,不如递归直观
 */
var maxPathSumIterative = function(root) {
    if (!root) return 0;
    
    let maxSum = -Infinity;
    const stack = [];
    const map = new Map(); // 存储节点与其最大贡献值的映射
    
    let lastVisited = null;
    let node = root;
    
    while (node || stack.length) {
        while (node) {
            stack.push(node);
            node = node.left;
        }
        
        node = stack[stack.length - 1];
        
        // 如果右子树存在且未被访问过
        if (node.right && lastVisited !== node.right) {
            node = node.right;
        } else {
            // 处理当前节点
            stack.pop();
            
            // 获取左右子树的最大贡献值
            const leftGain = Math.max(map.get(node.left) || 0, 0);
            const rightGain = Math.max(map.get(node.right) || 0, 0);
            
            // 计算当前节点作为顶点的路径和
            const currentPathSum = node.val + leftGain + rightGain;
            maxSum = Math.max(maxSum, currentPathSum);
            
            // 存储当前节点的最大贡献值
            const currentGain = node.val + Math.max(leftGain, rightGain);
            map.set(node, currentGain);
            
            lastVisited = node;
            node = null;
        }
    }
    
    return maxSum;
};

5. 复杂度与优缺点对比

5.1 复杂度分析表格

实现方式 时间复杂度 空间复杂度 代码简洁性 适用场景
递归DFS(最优解) O(n) O(h) ⭐⭐⭐⭐⭐ 绝大多数场景,代码直观易理解
迭代栈模拟 O(n) O(h) ⭐⭐ 深度极大可能栈溢出时使用
暴力枚举(不推荐) O(n²) O(n) 仅用于理解问题,实际不可用

5.2 各方法详细对比

  1. 递归DFS

    • 优点:代码简洁,逻辑清晰,符合树结构的自然处理方式
    • 缺点:在极端不平衡的树上可能导致调用栈溢出
    • 前端应用:适合处理DOM树、组件树等大多数前端树形结构
  2. 迭代栈模拟

    • 优点:避免递归栈溢出问题
    • 缺点:代码复杂,需要手动管理栈和访问状态
    • 前端应用:处理深度极大的树结构(如大型菜单树)
  3. 暴力枚举

    • 优点:思路简单,易于理解
    • 缺点:效率极低,仅适用于极小规模数据
    • 前端应用:基本不使用,但可用于验证算法正确性

6. 总结与前端应用场景

6.1 算法核心要点

  1. 分治思想:将大问题分解为子树的小问题
  2. 后序遍历:先处理子问题,再处理当前节点
  3. 全局状态:使用外部变量记录过程中的最优解
  4. 贪心选择:每个节点向上返回时只选择最优的一边

6.2 前端实际应用场景

场景一:组件树性能分析
javascript 复制代码
// 假设每个组件有渲染时间,找出渲染最慢的组件路径
class ComponentNode {
    constructor(name, renderTime) {
        this.name = name;
        this.renderTime = renderTime;
        this.children = [];
    }
    
    addChild(child) {
        this.children.push(child);
    }
}

function findSlowestRenderPath(root) {
    let maxTime = -Infinity;
    let slowestPath = [];
    
    const dfs = (node, currentPath) => {
        if (!node) return 0;
        
        currentPath.push(node.name);
        
        let maxChildTime = 0;
        for (const child of node.children) {
            const childTime = dfs(child, [...currentPath]);
            maxChildTime = Math.max(maxChildTime, childTime);
        }
        
        // 当前组件的总渲染时间(自身+最慢子组件)
        const totalTime = node.renderTime + maxChildTime;
        
        // 如果是叶子节点或者是更慢的路径
        if (node.children.length === 0 && totalTime > maxTime) {
            maxTime = totalTime;
            slowestPath = currentPath;
        }
        
        currentPath.pop();
        return totalTime;
    };
    
    dfs(root, []);
    return { maxTime, slowestPath };
}
场景二:DOM树最大属性值路径
javascript 复制代码
// 查找DOM树中某属性值之和最大的路径
function findMaxAttributePath(rootElement, attributeName) {
    let maxSum = -Infinity;
    let maxPath = [];
    
    const dfs = (element, currentPath) => {
        if (!element) return 0;
        
        // 获取当前元素的属性值
        const attrValue = parseInt(element.getAttribute(attributeName) || 0);
        currentPath.push(element.tagName);
        
        // 处理子元素
        const children = Array.from(element.children);
        let maxChildSum = 0;
        let bestChildPath = [];
        
        for (const child of children) {
            const childResult = dfs(child, []);
            if (childResult.sum > maxChildSum) {
                maxChildSum = childResult.sum;
                bestChildPath = childResult.path;
            }
        }
        
        // 计算当前路径总和
        const totalSum = attrValue + maxChildSum;
        const totalPath = [element.tagName, ...bestChildPath];
        
        // 更新全局最大值
        if (totalSum > maxSum) {
            maxSum = totalSum;
            maxPath = totalPath;
        }
        
        return { sum: totalSum, path: totalPath };
    };
    
    dfs(rootElement, []);
    return { maxSum, maxPath };
}
场景三:组织结构树的最大团队效能
javascript 复制代码
// 在组织架构树中找出效能最高的团队路径
class EmployeeNode {
    constructor(name, performance) {
        this.name = name;
        this.performance = performance; // 个人绩效
        this.subordinates = [];
    }
    
    addSubordinate(employee) {
        this.subordinates.push(employee);
    }
}

function findBestTeamPath(ceo) {
    let bestTeamScore = -Infinity;
    let bestTeamPath = [];
    
    const dfs = (employee) => {
        if (!employee) return { score: 0, path: [] };
        
        let maxSubordinateScore = 0;
        let bestSubordinatePath = [];
        
        // 计算所有下属团队的最佳贡献
        for (const subordinate of employee.subordinates) {
            const subordinateResult = dfs(subordinate);
            if (subordinateResult.score > maxSubordinateScore) {
                maxSubordinateScore = subordinateResult.score;
                bestSubordinatePath = subordinateResult.path;
            }
        }
        
        // 当前团队的总分(个人+最佳下属团队)
        const teamScore = employee.performance + maxSubordinateScore;
        const teamPath = [employee.name, ...bestSubordinatePath];
        
        // 更新最佳团队
        if (teamScore > bestTeamScore) {
            bestTeamScore = teamScore;
            bestTeamPath = teamPath;
        }
        
        // 返回给上级:选择最佳的下属团队贡献
        return { 
            score: Math.max(employee.performance, teamScore),
            path: teamScore > employee.performance ? teamPath : [employee.name]
        };
    };
    
    dfs(ceo);
    return { bestTeamScore, bestTeamPath };
}
相关推荐
摇滚侠2 小时前
面试实战 问题三十四 对称加密 和 非对称加密 spring 拦截器 spring 过滤器
java·spring·面试
talenteddriver2 小时前
java: Java8以后hashmap扩容后根据高位确定元素新位置
java·算法·哈希算法
yyy(十一月限定版)3 小时前
c语言——栈和队列
java·开发语言·数据结构
于是我说3 小时前
一份Python 面试常见问题清单 覆盖从初级到高级
开发语言·python·面试
跨境猫小妹3 小时前
2025 TikTok Shop:从内容爆发到系统化深耕的商业跃迁
大数据·人工智能·算法·产品运营·亚马逊
不穿格子的程序员3 小时前
从零开始写算法 —— 二叉树篇 1:二叉树的三种遍历(递归实现法)
算法·深度优先·二叉树遍历·fds
子夜江寒3 小时前
逻辑森林与贝叶斯算法简介
算法·机器学习
小妖6663 小时前
力扣(LeetCode)- 93. 复原 IP 地址(JavaScript)
javascript·tcp/ip·leetcode
xu_yule4 小时前
算法基础-背包问题(01背包问题)
数据结构·c++·算法·01背包