前端树形结构过滤算法

树形结构过滤是前端开发高频需求,广泛应用于菜单权限、文件管理、组织架构等场景。核心目标是:根据条件筛选节点,保留匹配节点及其完整父路径,同时保持树形结构不变、不修改原始数据

一、核心问题定义

输入输出

  • 输入:树形结构数组(每个节点含 name 和可选 children 字段)、过滤条件(如名称匹配)
  • 输出:新树形结构,仅包含「匹配节点 + 必要祖先节点」

数据格式示例

javascript 复制代码
const tree = [
  { name: 'A' },
  { name: 'B', children: [{ name: 'A' }, { name: 'D', children: [] }] },
  { name: 'C' }
];

核心约束

  1. 保持层级关系:匹配节点的所有祖先必须保留
  2. 数据不可变:不修改原始树,返回全新结构
  3. 完整性:匹配节点的子节点无需过滤(仅需保留路径)

二、最优实现方案

递归核心实现(推荐)

javascript 复制代码
/**
 * 树形结构过滤:保留匹配节点及其父路径
 * @param {Array} tree - 原始树形结构
 * @param {string} filterName - 过滤名称
 * @returns {Array} 过滤后的新树
 */
function filterTree(tree, filterName) {
  const filterNode = (node) => {
    // 1. 节点自身匹配:直接返回(含所有子节点)
    if (node.name === filterName) return { ...node };

    // 2. 递归处理子节点
    if (node.children?.length) {
      const filteredChildren = node.children.map(filterNode).filter(Boolean);
      // 子节点有匹配项:保留当前节点 + 过滤后的子节点
      if (filteredChildren.length) {
        return { ...node, children: filteredChildren };
      }
    }

    // 3. 无匹配:返回null(后续过滤)
    return null;
  };

  return tree.map(filterNode).filter(Boolean);
}

算法特性

  • 时间复杂度:O(n)(遍历所有节点一次)
  • 空间复杂度:O(h)(h为树高,递归栈开销)

迭代实现(大数据量适配)

解决深层树递归栈溢出问题:

javascript 复制代码
function filterTreeIterative(tree, filterName) {
  const stack = tree.map(node => ({ node, path: [] }));
  const result = [];

  while (stack.length) {
    const { node, path } = stack.pop();

    // 匹配节点:重建完整路径
    if (node.name === filterName) {
      let currentLevel = result;
      path.forEach(ancestor => {
        let exist = currentLevel.find(item => item.name === ancestor.name);
        if (!exist) currentLevel.push(exist = { ...ancestor, children: [] });
        currentLevel = exist.children;
      });
      currentLevel.push({ ...node });
    }

    // 子节点入栈(保持深度优先)
    node.children?.forEach(child => {
      stack.push({ node: child, path: [...path, node] });
    });
  }

  return result;
}

三、灵活扩展:通用过滤函数

支持模糊匹配、自定义子节点字段,适配复杂场景:

javascript 复制代码
/**
 * 通用树形过滤函数
 * @param {Array} tree - 原始树
 * @param {Function} predicate - 匹配函数(返回boolean)
 * @param {Object} options - 配置:{ childrenKey: 子节点字段名 }
 * @returns {Array} 过滤后新树
 */
function filterTreeUniversal(tree, predicate, options = {}) {
  const { childrenKey = 'children' } = options;

  const filterNode = (node) => {
    if (predicate(node)) return { ...node };

    const children = node[childrenKey];
    if (children?.length) {
      const filteredChildren = children.map(filterNode).filter(Boolean);
      if (filteredChildren.length) {
        return { ...node, [childrenKey]: filteredChildren };
      }
    }

    return null;
  };

  return tree.map(filterNode).filter(Boolean);
}

// 用法示例:模糊匹配含"A"的节点
const result = filterTreeUniversal(tree, node => 
  node.name.toLowerCase().includes('a')
);

四、前端实战场景

1. 文件管理器搜索

javascript 复制代码
const fileTree = [
  { name: 'src', children: [
    { name: 'components', children: [{ name: 'Button.js' }, { name: 'Modal.js' }] },
    { name: 'utils.js' }
  ]}
];

// 搜索"Button":返回 src/components/Button.js 完整路径
const searchResult = filterTree(fileTree, 'Button.js');

2. 组织架构筛选

javascript 复制代码
const orgTree = [
  { name: '技术部', children: [
    { name: '前端组', children: [{ name: '张三' }, { name: '李四' }] },
    { name: '后端组', children: [{ name: '王五' }] }
  ]}
];

// 筛选"前端"相关:保留技术部→前端组→所有成员
const deptResult = filterTreeUniversal(orgTree, node => 
  node.name.includes('前端')
);

3. 权限菜单过滤

javascript 复制代码
const allMenus = [
  { name: '系统管理', perm: 'admin', children: [
    { name: '用户管理', perm: 'user:manage' },
    { name: '角色管理', perm: 'role:manage' }
  ]}
];

// 根据用户权限过滤
const userPerms = ['user:manage'];
const menuResult = filterTreeUniversal(
  allMenus,
  node => userPerms.includes(node.perm),
  { childrenKey: 'children' }
);

五、性能优化要点

  1. 大数据量:使用迭代实现代替递归,避免栈溢出(树高>1000时)
  2. 频繁过滤:缓存计算结果,或使用虚拟列表只渲染可见节点
  3. 复杂匹配:提前预处理节点(如小写化名称),减少匹配时计算
  4. 按需加载:结合后端接口,只请求匹配路径的节点数据

六、测试关键用例

javascript 复制代码
// 1. 匹配叶子节点:返回完整路径
expect(filterTree(tree, 'D')[0].name).toBe('B');
expect(filterTree(tree, 'D')[0].children[0].name).toBe('D');

// 2. 匹配父节点:返回该节点及所有子节点
expect(filterTree(tree, 'B')[0].children.length).toBe(2);

// 3. 无匹配项:返回空数组
expect(filterTree(tree, 'X').length).toBe(0);

// 4. 不修改原始数据
const original = JSON.parse(JSON.stringify(tree));
filterTree(tree, 'A');
expect(tree).toEqual(original);

总结

树形结构过滤的核心是「递归遍历 + 路径保留」,关键要把握三点:

  1. 优先使用递归实现(简洁高效),大数据量场景切换迭代方案
  2. 严格遵循数据不可变原则,避免副作用
  3. 通过通用函数封装,适配不同业务场景(匹配规则、节点结构)
相关推荐
小龙报7 小时前
《算法通关指南:数据结构和算法篇 --- 顺序表相关算法题》--- 询问学号,寄包柜,合并两个有序数组
c语言·开发语言·数据结构·c++·算法·学习方法·visual studio
前端小咸鱼一条7 小时前
19. React的高阶组件
前端·javascript·react.js
狮子座的男孩7 小时前
js基础:10、函数对象方法(call/apply)、arguments类数组对象、Date对象、Math工具类、包装类、字符串方法、正则表达式
前端·javascript·正则表达式·包装类·字符串方法·arguments·date对象
小南家的青蛙8 小时前
LeetCode LCR 085 括号生成
算法·leetcode·职场和发展
jackzhuoa8 小时前
Rust 异步核心机制剖析:从 Poll 到状态机的底层演化
服务器·前端·算法
夜晚中的人海8 小时前
【C++】模拟算法习题
c++·算法·哈希算法
花月C8 小时前
算法 - 差分
人工智能·算法·机器学习
拆房老料8 小时前
深入解析提示语言模型校准:从理论算法到任务导向实践
人工智能·算法·语言模型
JIngJaneIL8 小时前
财务管理|基于SprinBoot+vue的个人财务管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·毕设·财务管理系统