0

0

递归更新树形结构中指定节点及其父节点的数值(排除根节点)

霞舞

霞舞

发布时间:2025-09-15 12:26:36

|

932人浏览过

|

来源于php中文网

原创

递归更新树形结构中指定节点及其父节点的数值(排除根节点)

本文介绍如何在JavaScript中,针对一个嵌套的树形数据结构,根据指定的唯一键值,递归地更新匹配节点及其所有祖先节点的 curr 属性,同时确保顶层(根)节点不被修改。通过一个带有深度参数和布尔返回值传播机制的递归函数,实现精确控制更新范围。

问题概述

在处理具有层级关系的树形数据时,我们经常需要根据某个特定节点的标识(例如 key)来更新其自身以及其所有父级节点的信息。一个典型的场景是,当子节点发生某个事件时,其父级节点也需要相应地汇总或更新状态。然而,在某些情况下,我们可能希望更新操作只影响到指定节点及其上级节点,而排除最顶层的根节点。

考虑以下这种嵌套的JavaScript对象数组结构:

const data = [
  {
    key: "id1",
    name: "Category 1",
    curr: 0,
    total: 0,
    nodes: [
      {
        key: "id2",
        name: "Applications",
        curr: 20,
        total: 30,
        nodes: [
          {
            key: "id3",
            name: "Gaming",
            curr: 5,
            total: 10,
            nodes: []
          },
          {
            key: "id4",
            name: "Operating System",
            curr: 15,
            total: 20,
            nodes: []
          }
        ]
      }
    ]
  },
  {
    key: "id5",
    name: "Category 2",
    curr: 0,
    total: 0,
    nodes: [
      {
        key: "id6",
        name: "Sub Category",
        curr: 12,
        total: 48,
        nodes: [
          {
            key: "id7",
            name: "Inside Sub",
            curr: 12,
            total: 48,
            nodes: []
          }
        ]
      }
    ]
  },
  {
    key: "id8",
    name: "Last One",
    curr: 0,
    total: 0,
    nodes: []
  }
];

每个节点都包含一个唯一的 key、name、curr(当前值)、total(总值)以及一个 nodes 数组,表示其子节点。我们的目标是,当传入一个 key 时,找到匹配的节点,将其 curr 值递增,并且将其所有父节点的 curr 值也递增,但递增最顶层(level 0)的节点。

例如,如果调用 incrementRecursively(data, "id4"),预期的输出是 id4 的 curr 从 15 变为 16,其父节点 id2 的 curr 从 20 变为 21。而 id1(level 0)的 curr 保持不变。

核心挑战与传统方法局限

直接的递归遍历通常只能在找到目标节点时进行操作,但无法直接通知其父节点进行更新。如果父节点需要根据子节点的状态进行更新,传统的 forEach 遍历或简单的递归函数难以实现这种“自下而上”的更新传播。此外,如何精确控制更新的层级(即排除根节点)也是一个需要巧妙处理的问题。

解决方案:带深度控制的递归更新

解决此类问题的关键在于利用递归函数的返回值来向上级传递状态信息,并引入一个深度参数来控制更新的边界。

算法思路:

Memories.ai
Memories.ai

专注于视频解析的AI视觉记忆模型

下载
  1. 定义一个递归函数,该函数接收当前节点数组、目标 key 和当前递归深度 depth。
  2. 遍历当前节点数组中的每个节点。
  3. 对于每个节点,检查其 key 是否与目标 key 匹配。
  4. 如果 key 匹配,或者对当前节点的子节点进行递归调用后返回 true(表示子节点中发生了更新),则说明当前节点是目标节点或其祖先节点。
  5. 在这种情况下,判断当前 depth 是否大于 0。如果 depth > 0,则说明当前节点不是根节点,可以安全地递增其 curr 值。
  6. 无论是否递增了 curr,都向上级返回 true,表示其子树中发生了更新。
  7. 如果遍历完当前层级的所有节点,都没有找到匹配项或子节点未更新,则返回 false。

代码实现:

/**
 * 递归更新树形结构中指定节点及其父节点的curr值,排除根节点。
 * @param {Array} nodes - 当前层级的节点数组。
 * @param {string} key - 要查找并更新的目标节点的key。
 * @param {number} depth - 当前节点的深度,根节点为0。
 * @returns {boolean} - 如果当前节点或其子节点中发生了更新,则返回true;否则返回false。
 */
function inc(nodes, key, depth = 0) {
    // 遍历当前层级的每一个节点
    for (let n of nodes) {
        // 检查当前节点的key是否匹配,或者其子节点(如果存在)中是否发生了更新
        // n.nodes ?? [] 用于处理nodes可能为null或undefined的情况
        if (n.key === key || inc(n.nodes ?? [], key, depth + 1)) {
            // 如果当前节点是匹配节点或其祖先节点,并且不是最顶层的根节点(depth > 0)
            if (depth > 0) {
                n.curr++; // 递增当前节点的curr值
            }
            // 返回true,向上层调用者表明在其子树中发生了更新
            return true;
        }
    }
    // 遍历完当前层级,没有找到匹配项,也没有子节点更新
    return false;
}

代码解析:

  • inc(nodes, key, depth = 0): 函数接收三个参数:当前层级的 nodes 数组,目标 key,以及一个 depth 参数,默认为 0,表示最顶层。
  • for (let n of nodes): 迭代当前 nodes 数组中的每个节点。
  • n.key === key || inc(n.nodes ?? [], key, depth + 1): 这是核心的判断逻辑。
    • n.key === key: 检查当前节点是否是我们要找的目标节点。
    • inc(n.nodes ?? [], key, depth + 1): 递归调用自身,处理当前节点的子节点。depth + 1 确保了深度参数正确递增。n.nodes ?? [] 使用空合并运算符,以防 n.nodes 为 null 或 undefined,确保递归调用时始终传入一个数组。
    • || 运算符:如果当前节点匹配,或者其任何子节点(或子节点的子节点)发生了更新(递归调用返回 true),则整个条件为真。这意味着当前节点是目标节点本身,或者它是目标节点的某个祖先节点。
  • if (depth > 0): 这是实现“排除根节点”的关键。只有当当前节点的深度大于 0 时(即它不是最顶层的节点),才允许递增其 curr 值。
  • n.curr++: 递增当前节点的 curr 值。
  • return true: 如果在当前节点或其子树中找到了目标 key 并进行了更新,则返回 true。这个 true 值会传递给上一层递归调用,使其父节点也能识别到子树中发生了更新,从而决定是否递增自身的 curr 值。
  • return false: 如果遍历完当前层级的所有节点,都没有找到匹配的 key,并且其子树中也没有发生更新,则返回 false。

使用示例

让我们使用上述 inc 函数来更新我们的 data 结构。

const data = [
  {
    key: "id1",
    name: "Category 1",
    curr: 0,
    total: 0,
    nodes: [
      {
        key: "id2",
        name: "Applications",
        curr: 20,
        total: 30,
        nodes: [
          {
            key: "id3",
            name: "Gaming",
            curr: 5,
            total: 10,
            nodes: []
          },
          {
            key: "id4",
            name: "Operating System",
            curr: 15,
            total: 20,
            nodes: []
          }
        ]
      }
    ]
  },
  {
    key: "id5",
    name: "Category 2",
    curr: 0,
    total: 0,
    nodes: [
      {
        key: "id6",
        name: "Sub Category",
        curr: 12,
        total: 48,
        nodes: [
          {
            key: "id7",
            name: "Inside Sub",
            curr: 12,
            total: 48,
            nodes: []
          }
        ]
      }
    ]
  },
  {
    key: "id8",
    name: "Last One",
    curr: 0,
    total: 0,
    nodes: []
  }
];

console.log("原始数据:", JSON.stringify(data, null, 2));

// 示例1: 更新 'id4'
inc(data, 'id4');
console.log("\n更新 'id4' 后的数据:", JSON.stringify(data, null, 2));
/* 预期输出:
  id4.curr: 15 -> 16
  id2.curr: 20 -> 21
  id1.curr: 0 (不变,因为 depth > 0 限制)
*/

// 示例2: 更新 'id7'
inc(data, 'id7');
console.log("\n更新 'id7' 后的数据:", JSON.stringify(data, null, 2));
/* 预期输出:
  id7.curr: 12 -> 13
  id6.curr: 12 -> 13
  id5.curr: 0 (不变)
*/

// 示例3: 尝试更新一个不存在的key
inc(data, 'nonExistentKey');
console.log("\n尝试更新不存在的key后的数据 (无变化):", JSON.stringify(data, null, 2));

注意事项

  • 数据变动性: 该 inc 函数会直接修改传入的 data 数组。如果需要保持原始数据的不可变性,应在函数内部对相关节点进行深拷贝,并返回一个新的数据结构。例如,可以使用 map 结合深拷贝来创建新的节点。
  • 键的唯一性: 解决方案依赖于 key 属性在整个树中的唯一性。如果 key 不唯一,可能会导致意外的更新。
  • 性能考量: 对于非常深或节点数量庞大的树,递归深度可能成为问题。但通常JavaScript引擎对递归深度有优化,对于一般应用场景是足够的。极端情况下,可以考虑将递归转换为迭代(例如使用栈)。
  • 错误处理: 如果传入的 key 不存在于树中,inc 函数将不会执行任何更新,并最终返回 false。可以根据需要添加额外的日志或错误抛出机制。
  • depth 参数的重要性: depth 参数是精确控制更新范围的关键。通过将其初始化为 0 并随着递归递增,我们能够区分根节点(depth === 0)和非根节点(depth > 0),从而实现有条件的更新。

总结

通过结合递归函数的返回值来传递更新状态和深度参数来控制更新范围,我们能够高效且精确地解决在树形结构中,根据指定键值递归更新节点及其祖先节点,同时排除根节点的问题。这种模式在处理复杂嵌套数据结构时非常有用,为树形数据的状态管理提供了一个强大的工具。理解并灵活运用这种递归策略,可以有效简化许多树形数据操作的逻辑。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

557

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

395

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

756

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

478

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

474

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

1051

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

658

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

554

2023.09.20

菜鸟裹裹入口以及教程汇总
菜鸟裹裹入口以及教程汇总

本专题整合了菜鸟裹裹入口地址及教程分享,阅读专题下面的文章了解更多详细内容。

0

2026.01.22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 4万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 2.4万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号