天天看点

过滤/筛选树节点

又是树,是我跟树杠上了吗?—— 不,是树的问题太多了! 🔗 相关文章推荐: 使用递归遍历并转换树形数据(以 typescript 为例) 从列表生成树 (javascript/typescript)

过滤和筛选是一个意思,都是 filter。

对于列表来说,过滤就是丢掉不需要的,留下需要的。但对于树来说就得分情况了。

如果想“过滤掉”(丢掉)某些节点,会把它的子节点一并抛弃,就像砍树枝一样,干之不存,枝将焉附?这种情况多是去除不需要的子树。

如果是想“查找”某些节点,会将找到的节点及其上溯到根的所有节点都保留下来。对于找到的节点,除了保留其完整路径之外,对其子树还有两种处理方式:

一种是“到此为止”,也就是说,如果其子树中没有符合条件的节点,那就不需要了,砍掉。需要定位到符合条件的节点以便后继操作是采用这种方式,这也是最常用的查找方式。

另一种是保留其完整子树。如果需要使用符合条件节点的子节点(比如选择指定部门及其子部门)会采用这种方式。

过滤和查找的主要区别在于:“过滤”通常会遇到不符合保留条件(或符合剔除条件)的节点就直接砍掉,不管其子树中是否还存在符合保留条件的节点;而查找则会一直找到叶节点上,只有整条路径都没有符合保留条件的节点,才会从其某个祖先节点上砍掉(祖先节点是否保留取决于其下是否存在符合保留条件的子孙节点)。

下面一样一样来。示例代码使用 typescript 编写,示例数据来源于从列表生成树 (javascript/typescript) 一文,同时使用该文中定义的节点类型接口:

过滤掉不需要的节点,思路比较简单:

遍历当前节点的所有子节点,需要的留,不需要的删

对留下的节点,通过递归进行过滤

按此思路,typescript 代码是

如果对示例数据(见前文)进行过滤,仅保留 <code>id</code> 是偶数的节点,那结果是

过滤/筛选树节点

不过这个 <code>filtertree</code> 有点小瑕疵:

递归调用时还需要传入 <code>predicate</code>,有点繁琐

传入参数应该限制在 <code>treenode[]</code> 类型上,添加 <code>undefined</code> 只是为了简化递归调用(不用先判空)

处理起来也简单,加一层接口封装一下(门面模式):

实际使用的时候,可能传入的可能是单根树 (<code>treenode</code>),也有可能是多根 (<code>treenode[]</code>),那可以写个重载:

查找节点就要稍微复杂了点了,因为需要保留路径。判断当前节点是否可以删除需要对自己情况进行判断之外,还取决于其所有子孙节点是否可以删除。与前面“过滤掉”的逻辑相比,有两点变化:

不管当前节点是否保留,均需要递归向下,把子孙中符合条件的节点都找出来

只要子孙中存在符合条件的节点,当前节点就应该保留。

这样处理后的节点,所有叶节点都应该符合查找条件。比如在示例数据中按 <code>id</code> 参整除 <code>6</code> 来查找节点,结果是:

过滤/筛选树节点

根据上面的逻辑,写一个 <code>findtreenode()</code>:

下面把代码修改下,在结果中保留子树。

这个思路跟最上面那个“剔除”的思路正好相反,

遇到符合条件的节点,直接保留整棵子树,也不需要递归去处理了

不符合条件的节点,递归进去继续找

既然都是查找,可以给 <code>findtreenode()</code> 添加一个 <code>keepsubtree: boolean</code> 参数来扩展函数功能。接口部分改变如下:

然后需要修改的地方主要是 <code>array.prototype.filter</code> 回调函数,可以先把原来的箭头函数提取出来,命名为 <code>filterwithoutsubtree()</code>。

过滤/筛选树节点

然后再写一个 <code>filterwithsubtree()</code> 处理函数。根据 <code>keepsubtree</code> 的值来决定使用哪一个过滤器。关键代码如下:

含完整子树的查找结果示例(查找条件:<code>it =&amp;gt; it.id % 4 === 0</code>)如下图:

过滤/筛选树节点

继续阅读