前端树形结构过滤算法

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

一、核心问题定义

输入输出

  • 输入:树形结构数组(每个节点含 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. 通过通用函数封装,适配不同业务场景(匹配规则、节点结构)
相关推荐
RFCEO几秒前
学习前端编程:DOM 树、CSSOM 树、渲染树详解
前端·学习·渲染树·dom 树·cssom 树·浏览器的渲染流程·回流/重绘
笨蛋不要掉眼泪3 分钟前
Redis主从复制:原理、配置与实战演示
前端·redis·bootstrap·html
孞㐑¥4 分钟前
算法—哈希表
开发语言·c++·经验分享·笔记·算法
bigdata-rookie6 分钟前
Starrocks 数据模型
java·前端·javascript
白帽子凯哥哥8 分钟前
网络安全Web基础完全指南:从小白到入门安全测试
前端·sql·web安全·信息安全·渗透测试·漏洞
RFCEO14 分钟前
前端编程 课程十四、:CSS核心基础2:选择器优先级 + 伪类选择器(解决冲突+交互效果)
前端·css·交互·css选择器优先级判断规则详解·css important使用·css链接伪类lvha顺序·实现悬浮交互效果
web打印社区16 分钟前
前端实现浏览器预览打印:从原生方案到专业工具
前端·javascript·vue.js·electron
近津薪荼19 分钟前
递归专题(2)——合并链表
c++·学习·算法·链表
Asher阿舍技术站19 分钟前
【数字通信理论系列】四、载波相位同步
算法·载波同步
-Try hard-24 分钟前
队列 | 二叉树
算法