It's a tree again. Did I get on the bar with the tree? ——No, there are too many problems with trees!
🔗 Recommended related articles:
Filtering and screening mean the same thing, both are filters.
For a list, filtering is to discard what is not needed and leave what is needed. But for the tree, it's the scoring situation.
- If you want to "filter out" (throw away) certain nodes, you will discard its child nodes as well, just like cutting a branch. If it doesn't exist, how will the branch be attached? In this case, unnecessary subtrees are removed.
If you want to "find" certain nodes, the found nodes and all the nodes up to the root will be retained. For the found node, in addition to preserving its full path, there are two other processing methods for its subtree:
- One is "stop here", that is, if there are no eligible nodes in its subtree, it is not needed and cut down. Need to locate the node that meets the conditions so that subsequent operations use this method, which is also the most commonly used search method.
- The other is to keep its complete subtree. If you need to use the sub-nodes of the eligible node (such as selecting a specified department and its sub-departments), this method will be used.
The main difference between filtering and searching is: "Filtering" usually encounters nodes that do not meet the retention conditions (or meets the elimination conditions) and directly cut them off, regardless of whether there are still nodes that meet the retention conditions in the subtree; while the search will The leaf node has been found, and only if the entire path does not have a node that meets the retention conditions, it will be cut off from one of its ancestor nodes (whether the ancestor node is retained depends on whether there are descendants that meet the retention conditions under it).
Do the same below. The sample code is written in TypeScript, and the sample data source 16162bd2da8d06 from the list to tree (JavaScript/TypeScript) article, and uses the node type interface defined in the article:
interface TreeNode {
id: number;
parentId: number;
label: string;
children?: TreeNode[]
}
Filter out unwanted nodes
The idea of filtering out unwanted nodes is relatively simple:
- Traverse all the child nodes of the current node, leave what is needed, delete what is not needed
- Filter the remaining nodes through recursion
According to this idea, the TypeScript code is
/**
* @param nodes 要过滤的树节点集(多根)
* @param predicate 过滤条件,返回 `true` 保留
* @returns 过滤后的树节点集
*/
function filterTree(
nodes: TreeNode[] | undefined,
predicate: (node: TreeNode) => boolean
): TreeNode[] | undefined {
if (!nodes?.length) { return nodes; }
// 直接使用 Array 的 filter 可以过滤当层节点
return nodes.filter(it => {
// 不符合条件的直接砍掉
if (!predicate(it)) {
return false;
}
// 符合条件的保留,并且需要递归处理其子节点
it.children = filterTree(it.children, predicate);
return true;
});
}
If the sample data (see above) is filtered and only id
is an even number, the result is
But this filterTree
a little flaw:
- You also need to pass in
predicate
when calling recursively, which is a bit cumbersome - The incoming parameters should be limited to the
TreeNode[]
, addingundefined
is just to simplify the recursive call (no need to judge empty first)
It is also simple to deal with, add a layer of interface encapsulation (facade mode):
/**
* @param nodes 要过滤的树节点集(多根)
* @param predicate 过滤条件,返回 `true` 保留
* @returns 过滤后的树节点集
*/
function filterTree(
nodes: TreeNode[],
predicate: (node: TreeNode) => boolean
): TreeNode[] {
return filter(nodes) ?? [];
// 原 filterTree,更名,并删除 predicate 参数
function filter(nodes: TreeNode[] | undefined): TreeNode[] | undefined {
if (!nodes?.length) { return nodes; }
return nodes.filter(it => {
if (!predicate(it)) {
return false;
}
// 递归调用不需要再传入 predicate
it.children = filter(it.children);
return true;
});
}
}
In actual use, it may be a single-rooted tree ( TreeNode
) or multiple roots ( TreeNode[]
). Then you can write an overload:
function filterTree(node: TreeNode, predicate: (node: TreeNode) => boolean): TreeNode;
function filterTree(nodes: TreeNode[], predicate: (node: TreeNode) => boolean): TreeNode[];
function filterTree(
tree: TreeNode | TreeNode[],
predicate: (node: TreeNode) => boolean
): TreeNode | TreeNode[] {
if (Array.isArray(tree)) {
return filter(tree) ?? [];
} else {
tree.children = filter(tree.children);
return tree;
}
function filter(...) { ... }
}
Find node (without complete subtree)
Finding a node is a bit more complicated, because the path needs to be preserved. Judging whether the current node can be deleted requires a judgment on its own situation, and also depends on whether all its descendants can be deleted. Compared with the previous "filtered out" logic, there are two changes:
- Regardless of whether the current node is retained or not, it needs to recursively downward to find out all the eligible nodes in the descendants
- As long as there are eligible nodes in the descendants, the current node should be retained.
All leaf nodes of the nodes processed in this way should meet the search conditions. For example, in the sample data, the id
parameter is divided by 6
to find the node. The result is:
According to the above logic, write a findTreeNode()
:
function findTreeNode(node: TreeNode, predicate: (node: TreeNode) => boolean): TreeNode;
function findTreeNode(nodes: TreeNode[], predicate: (node: TreeNode) => boolean): TreeNode[];
function findTreeNode(
tree: TreeNode | TreeNode[],
predicate: (node: TreeNode) => boolean
): TreeNode | TreeNode[] {
if (Array.isArray(tree)) {
return filter(tree) ?? [];
} else {
tree.children = filter(tree.children);
return tree;
}
function filter(nodes: TreeNode[] | undefined): TreeNode[] | undefined {
if (!nodes?.length) { return nodes; }
return nodes.filter(it => {
// 先筛选子树,如果子树中没有符合条件的,children 会是 [] 或 undefined
const children = filter(it.children);
// 根据当前节点情况和子树筛选结果判断是否保留当前节点
if (predicate(it) || children?.length) {
// 如果要保留,children 应该用筛选出来的那个;不保留的话就不 care 子节点了
it.children = children;
return true;
}
return false;
});
}
}
Let's modify the code to keep the subtree in the result.
Find node (including complete subtree)
This idea is exactly the opposite of the "removal" idea at the top.
- When encountering a node that meets the conditions, the entire subtree is directly retained, and there is no need to recursively process
- Nodes that do not meet the conditions, recursively go in and continue to find
Since they are to find, you can give findTreeNode()
add a keepSubTree: boolean
parameters to extend the functional capabilities. The interface changes are as follows:
function findTreeNode(
node: TreeNode,
predicate: (node: TreeNode) => boolean,
keepSubTree?: boolean // <--
): TreeNode;
function findTreeNode(
nodes: TreeNode[],
predicate: (node: TreeNode) => boolean,
keepSubTree?: boolean // <--
): TreeNode[];
function findTreeNode(
tree: TreeNode | TreeNode[],
predicate: (node: TreeNode) => boolean,
keepSubTree: boolean = false // <--
): TreeNode | TreeNode[] {
...
}
Then the main area that needs to be modified is the Array.prototype.filter
callback function. You can extract the original arrow function first and name it filterWithoutSubTree()
.
Then write a filterWithSubTree()
processing function. Decide which filter to use according to keepSubTree
The key code is as follows:
function findTreeNode(...): TreeNode | TreeNode[] {
const filterHandler = keepSubTree ? filterWithSubTree : filterWithoutSubTree;
// ^^^^^^^^^^^^^
if (Array.isArray(tree)) { ... } else { ... }
function filter(nodes: TreeNode[] | undefined): TreeNode[] | undefined {
if (!nodes?.length) { return nodes; }
return nodes.filter(filterHandler);
// ^^^^^^^^^^^^^
}
function filterWithSubTree(it: TreeNode): boolean {
// 如果符合条件,保留整棵子树,不需要递归进去
if (predicate(it)) { return true; }
// 否则根据子孙节点的情况来决定是否需要保留当前节点(作为路径节点)
it.children = filter(it.children);
return !!it.children?.length;
}
function filterWithoutSubTree(it: TreeNode): boolean {
...
}
}
An example of the search result with a complete subtree (search condition: it => it.id % 4 === 0
) is as follows:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。