20
头图

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

flowchart LR
%%{ init: { "theme": "forest" } }%%

S(("Virtual\nRoot"))
S --> N6
S --> N10

N6("6 | P6mtcgfCD")
N6 --> N8("8 | m6o5UsytQ0")
N10("10 | lhDGTNeeSxLNJ")
N6 --> N14("14 | ysYwG8EFLAu1a")
N10 --> N16("16 | RKuQs4ki65wo")

But this filterTree a little flaw:

  1. You also need to pass in predicate when calling recursively, which is a bit cumbersome
  2. The incoming parameters should be limited to the TreeNode[] , adding undefined 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:

  1. 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
  2. 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:

flowchart LR
%%{ init: { "theme": "forest" } }%%
classDef found fill:#ffeeee,stroke:#cc6666;

S(("Virtual\nRoot")) --> N1
S --> N6:::found;

N1("1 | 8WUg35y")
N1 --> N4("4 | IYkxXlhmU12x")
N4 --> N5("5 | p2Luabg9mK2")
N6("6 | P6mtcgfCD")
N1 --> N7("7 | yluJgpnqKthR")
N7 --> N12("12 | 5W6vy0EuvOjN"):::found
N5 --> N13("13 | LbpWq")
N13 --> N18("18 | 03X6e4UT"):::found

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:

flowchart LR
%%{ init: { "theme": "forest" } }%%
classDef found fill:#ffeeee,stroke:#cc6666;
classDef subs fill:#ffffff;

S(("Virtual\nRoot")) --> N1
S --> N6
S --> N10

N1("1 | 8WUg35y")
N1 --> N4("4 | IYkxXlhmU12x"):::found
N4 --> N5("5 | p2Luabg9mK2"):::subs
N6("6 | P6mtcgfCD")
N1 --> N7("7 | yluJgpnqKthR")
N6 --> N8("8 | m6o5UsytQ0"):::found
N10("10 | lhDGTNeeSxLNJ")
N7 --> N12("12 | 5W6vy0EuvOjN"):::found
N5 --> N13("13 | LbpWq"):::subs
N10 --> N16("16 | RKuQs4ki65wo"):::found
N13 --> N18("18 | 03X6e4UT"):::subs
N7 --> N19("19 | LTJTeF")
N19 --> N20("20 | 3rqUqE3MLShh"):::found

边城
59.8k 声望29.6k 粉丝

一路从后端走来,终于走在了前端!