如何使用 JavaScript 在树中查找节点

How to find a node in a tree with JavaScript

本文关键字:查找 节点 何使用 JavaScript      更新时间:2023-09-26

我有和对象文字,本质上是一棵没有固定级别数的树。我怎样才能在树中搜索一个特定的节点,然后在javascript中以有效的方式找到该节点时返回该节点?

本质上,我有一棵这样的树,并希望找到标题为"randomNode_1"的节点

var data = [
{
title: 'topNode',
 children: [
   {
       title: 'node1',
       children: [
       {
           title: 'randomNode_1'
       },
       {   
           title: 'node2',
           children: [
           {
               title: 'randomNode_2',
               children:[
               {   
                   title: 'node2',
                   children: [
                   {
                       title: 'randomNode_3',
                   }]
               }
               ]
           }]
       }]
   }
  ]
 }];

这个答案基于@Ravindra的答案,但具有真正的递归。

function searchTree(element, matchingTitle){
     if(element.title == matchingTitle){
          return element;
     }else if (element.children != null){
          var i;
          var result = null;
          for(i=0; result == null && i < element.children.length; i++){
               result = searchTree(element.children[i], matchingTitle);
          }
          return result;
     }
     return null;
}

然后你可以称之为:

var element = data[0];
var result = searchTree(element, 'randomNode_1');

这是一个迭代解决方案:

var stack = [], node, ii;
stack.push(root);
while (stack.length > 0) {
    node = stack.pop();
    if (node.title == 'randomNode_1') {
        // Found it!
        return node;
    } else if (node.children && node.children.length) {
        for (ii = 0; ii < node.children.length; ii += 1) {
            stack.push(node.children[ii]);
        }
    }
}
// Didn't find it. Return null.
return null;

这是一个使用 Stack 方法的迭代函数,灵感来自 FishBasketGordo 的答案,但利用了一些 ES2015 语法来缩短内容。

由于这个问题已经被查看了很多次,我决定更新我的答案,以提供一个带有参数的函数,使其更加灵活:

function search (tree, value, key = 'id', reverse = false) {
  const stack = [ tree[0] ]
  while (stack.length) {
    const node = stack[reverse ? 'pop' : 'shift']()
    if (node[key] === value) return node
    node.children && stack.push(...node.children)
  }
  return null
}

这样,现在可以将数据传递tree本身、要搜索的所需value以及可以具有所需值的属性key

search(data, 'randomNode_2', 'title')

最后,我最初的答案使用了Array.pop,这会导致在多个匹配的情况下匹配最后一项。事实上,这可能真的很令人困惑。受到Superole评论的启发,我现在使用它Array.shift,所以先进先出的行为是默认的。

如果您真的想要旧的后进先出行为,我提供了一个额外的参数reverse

search(data, 'randomNode_2', 'title', true)

我的回答灵感来自FishBasketGordo的迭代答案。它稍微复杂一些,但也更灵活,您可以拥有多个根节点。

/**searchs through all arrays of the tree if the for a value from a property
 * @param aTree : the tree array
 * @param fCompair : This function will receive each node. It's upon you to define which 
                     condition is necessary for the match. It must return true if the condition is matched. Example:
                        function(oNode){ if(oNode["Name"] === "AA") return true; }
 * @param bGreedy? : us true to do not stop after the first match, default is false
 * @return an array with references to the nodes for which fCompair was true; In case no node was found an empty array
 *         will be returned
*/
var _searchTree = function(aTree, fCompair, bGreedy){
    var aInnerTree = []; // will contain the inner children
    var oNode; // always the current node
    var aReturnNodes = []; // the nodes array which will returned
    // 1. loop through all root nodes so we don't touch the tree structure
    for(keysTree in aTree) {
        aInnerTree.push(aTree[keysTree]);
    }
    while(aInnerTree.length > 0) {
        oNode = aInnerTree.pop();
        // check current node
        if( fCompair(oNode) ){
            aReturnNodes.push(oNode);
            if(!bGreedy){
                return aReturnNodes;
            }
        } else { // if (node.children && node.children.length) {
            // find other objects, 1. check all properties of the node if they are arrays
            for(keysNode in oNode){
                // true if the property is an array
                if(oNode[keysNode] instanceof Array){
                    // 2. push all array object to aInnerTree to search in those later
                    for (var i = 0; i < oNode[keysNode].length; i++) {
                        aInnerTree.push(oNode[keysNode][i]);
                    }
                }
            }
        }
    }
    return aReturnNodes; // someone was greedy
}

最后,您可以像这样使用该函数:

var foundNodes = _searchTree(data, function(oNode){ if(oNode["title"] === "randomNode_3") return true; }, false);
console.log("Node with title found: ");
console.log(foundNodes[0]);

如果你想找到所有具有这个标题的节点,你可以简单地切换bGreedy参数:

var foundNodes = _searchTree(data, function(oNode){ if(oNode["title"] === "randomNode_3") return true; }, true);
console.log("NodeS with title found: ");
console.log(foundNodes);

在树中查找节点:

假设我们有一棵树,

比如
let tree = [{
  id: 1,
  name: 'parent',
  children: [
    {
      id: 2,
      name: 'child_1'
    },
    {
      id: 3,
      name: 'child_2',
      children: [
        {
          id: '4',
          name: 'child_2_1',
          children: []
        },
        {
          id: '5',
          name: 'child_2_2',
          children: []
        }
      ]
    }
  ]
}];

function findNodeById(tree, id) {
   let result = null
   if (tree.id === id) {
        return tree;
   }
   if (Array.isArray(tree.children) && tree.children.length > 0) {
      tree.children.some((node) => {
        result = findNodeById(node, id);
        return result;
      });
   }
   return result;}

你必须使用递归。

var currChild = data[0];
function searchTree(currChild, searchString){
     if(currChild.title == searchString){
          return currChild;
     }else if (currChild.children != null){
          for(i=0; i < currChild.children.length; i ++){
               if (currChild.children[i].title ==searchString){
                    return currChild.children[i];
               }else{
                    searchTree(currChild.children[i], searchString);
               }
          }
          return null;
     }
     return null;
}

ES6+:

const deepSearch = (data, value, key = 'title', sub = 'children', tempObj = {}) => {
    if (value && data) {
        data.find((node) => {
            if (node[key] == value) {
                tempObj.found = node;
                return node;
            }
            return deepSearch(node[sub], value, key, sub, tempObj);
        });
        if (tempObj.found) {
            return tempObj.found;
        }
    }
    return false;
};
const result = deepSearch(data, 'randomNode_1', 'title', 'children');

此函数是通用的,并且以递归方式进行搜索。如果输入树是对象(单根(还是对象数组(许多根对象(,这并不重要。您可以配置在树对象中保存子数组的 prop 名称。

// Searches items tree for object with specified prop with value
// 
// @param {object} tree nodes tree with children items in nodesProp[] table, with one (object) or many (array of objects) roots
// @param {string} propNodes name of prop that holds child nodes array
// @param {string} prop name of searched node's prop
// @param {mixed} value value of searched node's  prop
// @returns {object/null} returns first object that match supplied arguments (prop: value) or null if no matching object was found
function searchTree(tree, nodesProp, prop, value) {
  var i, f = null; // iterator, found node
  if (Array.isArray(tree)) { // if entry object is array objects, check each object
    for (i = 0; i < tree.length; i++) {
      f = searchTree(tree[i], nodesProp, prop, value);
      if (f) { // if found matching object, return it.
        return f;
      }
    }
  } else if (typeof tree === 'object') { // standard tree node (one root)
    if (tree[prop] !== undefined && tree[prop] === value) {
      return tree; // found matching node
    }
  }
  if (tree[nodesProp] !== undefined && tree[nodesProp].length > 0) { // if this is not maching node, search nodes, children (if prop exist and it is not empty)
    return searchTree(tree[nodesProp], nodesProp, prop, value);
  } else {
    return null; // node does not match and it neither have children
  }
}

我在本地测试了它,它工作正常,但它不知何故无法在 jsfiddle 或 jsbin 上运行......(这些网站上的重复出现问题??

运行代码:

    var data = [{
      title: 'topNode',
      children: [{
        title: 'node1',
        children: [{
          title: 'randomNode_1'
        }, {
          title: 'node2',
          children: [{
            title: 'randomNode_2',
            children: [{
              title: 'node2',
              children: [{
                title: 'randomNode_3',
              }]
            }]
          }]
        }]
      }]
    }];
    var r = searchTree(data, 'children', 'title', 'randomNode_1');
    //var r = searchTree(data, 'children', 'title', 'node2');  // check it too
    console.log(r);

它可以在 http://www.pythontutor.com/live.html#mode=edit 中工作(粘贴代码(

no BS 版本:

const find = (root, title) => 
  root.title === title ?
    root :
    root.children?.reduce((result, n) => result || find(n, title), undefined)

用法:

const data = {
  title: "root",
  children: [
    { title: "ch1" },
    { title: "ch2", children: [{ title: "ch21" }] }
  ]
}
find(data, 'ch21') // returns { title: 'ch21' }

这是基本的递归问题。

window.parser = function(searchParam, data) {
  if(data.title != searchParam) {
    returnData = window.parser(searchParam, children)
  } else {
     returnData = data;
  }
  return returnData;
}

这是一个更复杂的选项 - 它找到树状节点中的第一项,并提供(node,nodeChildrenKey,键/值对和可选的附加键/值对(

const findInTree = (node, childrenKey, key, value,  additionalKey?, additionalValue?) => {
  let found = null;
  if (additionalKey && additionalValue) {
    found = node[childrenKey].find(x => x[key] === value && x[additionalKey] === additionalValue);
  } else {
    found = node[childrenKey].find(x => x[key] === value);
  }
  if (typeof(found) === 'undefined') {
    for (const item of node[childrenKey]) {
      if (typeof(found) === 'undefined' && item[childrenKey] && item[childrenKey].length > 0) {
        found = findInTree(item, childrenKey, key, value, additionalKey, additionalValue);
      }
    }
  }
  return found;
};
export { findInTree };

希望它对某人有所帮助。

在 2022 年使用 TypeScript 和 ES5

只需使用基本的重新创建和内置数组方法来循环数组。不要使用 Array.find(),因为这会返回错误的节点。请改用Array.some(),这样可以打破循环。

 interface iTree {
     id: string;
     children?: iTree[];
 }
function findTreeNode(tree: iTree, id: string) {
    let result: iTree | null = null;
    if (tree.id === id) {
        result = tree;
    } else if (tree.children) {
        tree.children.some((node) => {
            result = findTreeNode(node, id);
            return result; // break loop
        });
    }
    return result;
}

一个灵活的递归解决方案,适用于任何树

// predicate: (item) => boolean
// getChildren: (item) => treeNode[]
searchTree(predicate, getChildren, treeNode) {
        function search(treeNode) {
            if (!treeNode) {
                return undefined;
            }
            for (let treeItem of treeNode) {
                if (predicate(treeItem)) {
                    return treeItem;
                }
                const foundItem = search(getChildren(treeItem));
                if (foundItem) {
                    return foundItem;
                }
            }
        }
        return search(treeNode);
    }

在树中找到元素的所有父元素

let objects = [{
      id: 'A',
      name: 'ObjA',
      children: [
        {
          id: 'A1',
          name: 'ObjA1'
        },
        {
          id: 'A2',
          name: 'objA2',
          children: [
            {
              id: 'A2-1',
              name: 'objA2-1'
            },
            {
              id: 'A2-2',
              name: 'objA2-2'
            }
          ]
        }
      ]
    },
    {
      id: 'B',
      name: 'ObjB',
      children: [
        {
          id: 'B1',
          name: 'ObjB1'
        }
      ]
    }
    ];
let docs = [
  {
    object: {
      id: 'A',
      name: 'docA'
    },
    typedoc: {
      id: 'TD1',
      name: 'Typde Doc1'
    }
  },
  {
    object: {
      id: 'A',
      name: 'docA'
    },
    typedoc: {
      id: 'TD2',
      name: 'Typde Doc2'
    }
  },
  {
    object: {
      id: 'A1',
      name: 'docA1'
    },
    typedoc: {
      id: 'TDx1',
      name: 'Typde Doc x1'
    }
  },
  {
    object: {
      id: 'A1',
      name: 'docA1'
    },
    typedoc: {
      id: 'TDx2',
      name: 'Typde Doc x1'
    }
  },
  {
    object: {
      id: 'A2',
      name: 'docA2'
    },
    typedoc: {
      id: 'TDx2',
      name: 'Type de Doc x2'
    }
  },
  {
    object: {
      id: 'A2-1',
      name: 'docA2-1'
    },
    typedoc: {
      id: 'TDx2-1',
      name: 'Type de Docx2-1'
    },
  },
  {
    object: {
      id: 'A2-2',
      name: 'docA2-2'
    },
    typedoc: {
      id: 'TDx2-2',
      name: 'Type de Docx2-2'
    },
  },
  {
    object: {
      id: 'B',
      name: 'docB'
    },
    typedoc: {
      id: 'TD1',
      name: 'Typde Doc1'
    }
  },
  {
    object: {
      id: 'B1',
      name: 'docB1'
    },
    typedoc: {
      id: 'TDx1',
      name: 'Typde Doc x1'
    }
  }
];

function buildAllParents(doc, objects) {
    for (let o = 0; o < objects.length; o++) {
      let allParents = [];
      let getAllParents = (o, eleFinded) => {
        if (o.id === doc.object.id) {
          doc.allParents = allParents;
          eleFinded = true;
          return { doc, eleFinded };
        }
        if (o.children) {
          allParents.push(o.id);
          for (let c = 0; c < o.children.length; c++) {
            let { eleFinded, doc } = getAllParents(o.children[c], eleFinded);
            if (eleFinded) {
              return { eleFinded, doc };
            } else {
              continue;
            }
          }
        }
        return { eleFinded };
      };
      if (objects[o].id === doc.object.id) {
        doc.allParents = [objects[o].id];
        return doc;
      } else if (objects[o].children) {
        allParents.push(objects[o].id);
        for (let c = 0; c < objects[o].children.length; c++) {
          let eleFinded = null;`enter code here`
          let res = getAllParents(objects[o].children[c], eleFinded);
          if (res.eleFinded) {
            return res.doc;
          } else {
            continue;
          }
        }
      }
    }
  }
docs = docs.map(d => buildAllParents(d, objects`enter code here`))

这是一个迭代广度优先搜索。它返回包含给定名称 (nodeName( 和给定值 (nodeValue( 的子节点的第一个节点。

getParentNode(nodeName, nodeValue, rootNode) {
    const queue= [ rootNode ]
    while (queue.length) {
        const node = queue.shift()
        if (node[nodeName] === nodeValue) {
            return node
        } else if (node instanceof Object) {
            const children = Object.values(node)
            if (children.length) {
                queue.push(...children)
            }
        }
    }
    return null
}

它将像这样用于解决原始问题:

getParentNode('title', 'randomNode_1', data[0])

基于 "Erick Petrucelli" 的代码增强

  1. 删除"反向"选项
  2. 添加多根支持
  3. 添加用于控制"子项"可见性的选项
  4. 打字稿就绪
  5. 单元测试就绪
function searchTree(
  tree: Record<string, any>[],
  value: unknown,
  key = 'value',
  withChildren = false,
) {
  let result = null;
  if (!Array.isArray(tree)) return result;
  for (let index = 0; index < tree.length; index += 1) {
    const stack = [tree[index]];
    while (stack.length) {
      const node = stack.shift()!;
      if (node[key] === value) {
        result = node;
        break;
      }
      if (node.children) {
        stack.push(...node.children);
      }
    }
    if (result) break;
  }
  if (withChildren !== true) {
    delete result?.children;
  }
  return result;
}

测试可以在以下位置找到: https://gist.github.com/aspirantzhang/a369aba7f84f26d57818ddef7d108682

根据我的需要写了另一个

  1. 条件被注入。
  2. 找到的分支的路径可用
  3. 可以在条件语句中使用当前路径
  4. 可用于将树项映射到另一个对象
// if predicate returns true, the search is stopped
function traverse2(tree, predicate, path = "") {
  if (predicate(tree, path)) return true;
  for (const branch of tree.children ?? [])
    if (traverse(branch, predicate, `${path ? path + "/" : ""}${branch.name}`))
      return true;
}

let tree = {
  name: "schools",
  children: [
    {
      name: "farzanegan",
      children: [
        {
          name: "classes",
          children: [
            { name: "level1", children: [{ name: "A" }, { name: "B" }] },
            { name: "level2", children: [{ name: "C" }, { name: "D" }] },
          ],
        },
      ],
    },
    { name: "dastgheib", children: [{ name: "E" }, { name: "F" }] },
  ],
};
traverse(tree, (branch, path) => {
  console.log("searching ", path);
  if (branch.name === "C") {
    console.log("found ", branch);
    return true;
  }
});

输出

searching  
searching  farzanegan
searching  farzanegan/classes
searching  farzanegan/classes/level1
searching  farzanegan/classes/level1/A
searching  farzanegan/classes/level1/B
searching  farzanegan/classes/level2
searching  farzanegan/classes/level2/C
found  { name: 'C' }
const flattenTree = (data: any) => {
  return _.reduce(
    data,
    (acc: any, item: any) => {
      acc.push(item);
      if (item.children) {
        acc = acc.concat(flattenTree(item.children));
        delete item.children;
      }
     return acc;
    },
    []
    );
   };

将嵌套树转换为深度为 0 的对象的方法。我们可以在这样的对象中转换对象,并且可以更轻松地执行搜索。

以下内容在我这边工作:

function searchTree(data, value) {
if(data.title == value) {
    return data;
}
if(data.children && data.children.length > 0) {
    for(var i=0; i < data.children.length; i++) {
        var node = traverseChildren(data.children[i], value);
        if(node != null) {
            return node;
        }
    }
}
return null;

}