树形递归结构过滤算法

Posted by Yinode on Wednesday, October 27, 2021

TOC

对某个树形结构进行过滤是一个比较常见的场景,但同时想要写出没有问题,并且简洁的代码也并非易事,本文将对这种算法进行简单的分析,并在最后给出代码实例和使用实例。

这是一个树形结构,接下来我会基于这个树进行讲解

            A
          /   \
        B       C
       / \
      E   D       

假设我们拥有一个树形递归结构,其中节点内部存放的是某些数据,每个节点会拥有任意数量的子节点,接下来我们需要对整个树进行裁剪。

我们可以发现有两种基本的裁剪模式

1. 父元素优先,从上到下

这种裁剪的特点是,当一个父节点经过判断之后,发现是一个可以被保留的节点的时候,会直接保留整个子树,如果这个父节点自身并不满足保留条件,那么会继续向下递归检查,如果有一个子节点是满足保留条件的,这个父节点也会保留。

假设我们进行对上面的树进行筛选条件为,名字里面包含字母B,那么结果为

            A
          /   
        B       
       / \
      E   D   

这种情况比较适用于对于关键字进行筛选的功能,比如搜索某个部门的名称,同时也要保留部门下面的所有人员

1. 子元素优先,从下到上

这种裁剪的特点,其实就是上方的模式,但是取消父节点符合保留条件下,对于子树的直接保留,必须进一步对子树进行筛选

假设我们进行对上面的树进行筛选条件为,名字里面包含字母B,那么结果为

            A
          /   
        B    

假设我们进行对上面的树进行筛选条件为,名字里面包含字母E,那么结果为

            A
          /   
        B       
       / 
      E     

这种筛选比较适用于对于叶子节点进行类型筛选,比如想要筛选类型为X的所有人员,这种情况下是不允许整个树的叶子节点包含任何非该类型的节点的。

代码

有了基本顺序的分析,我们就能比较好的写出一个比较通用的过滤算法

const filterTreeData = (data, isFilterIn, childKey, isParentSave) => {
  if (!data) return null
  // 由于启动父优先级裁剪,如果匹配成功,保留所有子节点
  if (isParentSave && isFilterIn(data)) {
    return data
  }
  const children = (data && data[childKey]) ? data[childKey].map((child) => filterTreeData(child, isFilterIn, childKey, isParentSave)).filter(v => !!v) : []
  // 该节点本身匹配失败,并且也没有子节点能够匹配
  if (children.length === 0 && !isFilterIn(data)) return null
  return {
    ...data,
    children
  }
}

使用实例

实际使用的时候,我们可以进行多次过滤,来组合使用,比如先进行关键字筛选,再进行类型筛选,想要在一次递归之中完成两种不同的裁剪方式(上面的两种过滤裁剪模式)是不可能的。

// 先进行关键字筛选
let res = filterTreeData(treeData[0], (data) => {
  if (!keyword) return true
  return data[labelKey].includes(keyword)
}, childKey, true)
// 再进行类型筛选
res = filterTreeData(res, data => {
  if (activeTabName === 'doctor') return data[titleKey] === '医生'
  if (activeTabName === 'nurse') return data[titleKey] === '护士'
  return true
}, childKey, false)