如何在 JavaScript 中递归遍历通用树不是 BST?

问题描述

假设我们想要对公司中存在的层次结构进行建模。最基本的数据结构称为employee。每个 employee 都是它自己的一个对象/树,并具有两个属性level一个整数,reports一个包含 0 个或多个 employees 的对象/树。

我们可以说 employee 数据结构如下所示:

{
    employee: {
        level: Number,reports: Object //Contains 0 to N employees
    }
}

小型企业可以表示其组织如下:

{
boss: {
  level: 0,reports: {
    employee0: {
      level: 1,reports: {
        anotherEmployee0: {
          level: 2,reports: { /* ... */ },},anotherEmployee0: {
          level: 2,employee1: {
      level: 1,};

我们如何使用递归遍历这个结构?理想情况下,在没有语言函数抽象(例如 for ... inforeachmap)的情况下执行此操作。所以,“手动”。我很想看看这两种解决方案(有和没有所述抽象),以进行比较。

解决方法

for..in 是一种核心语言结构,从一开始就是 JavaScript 语言的一部分。这是提供的第一种检查对象属性的方法。

我在这里提供了两种解决方案。第一个将坚持您的数据结构,并使用 for..in 读取给定对象的属性并对 reports 值执行条件递归。它调用用户提供的回调函数,因此用户可以决定如何处理迭代值。

第二个切换到更好的数据结构,因为最好不要使用对象属性名称来表示动态内容。而是向您分配动态内容(“employee1”、“boss”等)的对象(可能是“名称”)添加一个属性。与老式的回调系统不同,它将函数定义为生成器。

解决方案 1(for..in + 回调):

这与 ES3 兼容。

function traverse(org,callback,parent) {
    for (var name in org) {
        var obj = org[name];
        callback(name,obj.level,parent);
        if (obj.reports) {
            traverse(obj.reports,name);
        }
    }
}

// demo with original data structure
var org = {boss: {level: 0,reports: {employee0: {level: 1,reports: {anotherEmployee0: {level: 2,reports: { /* ... */ },},anotherEmployee1: {level: 2,employee1: {level: 1,};

traverse(org,visit,null);

// Our function that processes the iterated values:
function visit(name,level,parent) {
    console.log("         ".slice(0,level) + name + " (level " + level + ") under " + (parent || "no one"));
}

方案二:(更好的数据结构+生成器)

这也使用解构、for..of 循环、模板文字、repeatlet、默认参数值、...

function * iterate(org,parent=null) {
    for (let {name,reports} of org) {
        yield [name,parent];
        if (reports) {
            yield * iterate(reports,name);
        }
    }
}

let org2 = [{ 
    name: "boss",level: 0,reports: [{ 
        name: "employee0",level: 1,reports: [{ 
            name: "anotherEmployee0",level: 2,reports: [/* ... */]
        },{ 
            name: "anotherEmployee1",reports: [/* ... */]
        }]
    },{
        name: "employee1",reports: [/* ... */]
    }]
}];

for (let [name,parent] of iterate(org2)) {
    console.log(`${" ".repeat(level)}${name} (level ${level}) under ${parent || "no one"}`);
}

,

Trincot 的出色回答使用了生成器“而不是老式的回调系统”。

以下是使用 trincot 对输入数据的重构来演示旧式系统如何工作的示例:

const traverseOrg = (fn) => (org,boss = null) =>
  org .forEach (emp => {
    fn (emp,boss)
    traverseOrg (fn) (emp.reports || [],emp) 
  })

let org2 = [{name: "boss",reports: [{name: "employee0",reports: [{name: "anotherEmployee0",reports: [/* ... */]},{name: "anotherEmployee1",reports: [/* ... */]}]},{name: "employee1",reports: [/* ... */]}]}];

traverseOrg (
  ({level,name},boss) => console .log (
    `${'    ' .repeat (level)}${name} (level ${level}) reports to ${boss ? boss.name : "no one"}`
  )
) (org2)

我们的 traverseOrg 函数接受一个回调函数并生成一个函数,该函数接受一组员工及其嵌套报告,并在深度优先遍历中调用每个员工的回调,传递员工和父员工,如果不存在则为 null。

我们可以将其用于与 trincot 演示的相同类型的输出,只需记录员工姓名、级别和老板姓名。

请注意,我们在这里使用 forEach。我们可以使用多种类型的循环构造,但由于我们将其视为简单的遍历而不是转换,因此 .map.reduce 等数组函数在此处不合适。

我们可以在此之上分层一个扁平化函数,如下所示:

const flattenOrg = (org) => {
  const res = []
  traverseOrg (({name,reports = []}) => res .push (
    {name,directReports: reports .map (({name}) => name)}
  )) (org)
  return res
}

flattenOrg (org2)

将返回此结构:

[
  {name: "boss",directReports: ["employee0","employee1"]},{name: "employee0",directReports: ["anotherEmployee0","anotherEmployee1"]},{name: "anotherEmployee0",directReports: []},directReports: []}
]

但这没有太多理由。这是丑陋的代码,我们最好直接编写它:

const flattenOrg = (org) =>
  org .flatMap (({name,reports = []}) => 
    [{name,directReports: reports .map (({name}) => name)},... flattenOrg (reports)]
  )