递归遍历家谱时遇到问题

Posted

技术标签:

【中文标题】递归遍历家谱时遇到问题【英文标题】:Having issues recursively traversing family tree 【发布时间】:2022-01-15 22:08:30 【问题描述】:

我有一个 Person 类,它具有以下属性:姓名、出生和后代,以其他 Person 类数组的形式出现。

现在我正在尝试实现一个函数,该函数可以从原始 Person 类开始遍历这个家谱,并返回一个数组,该数组包含所有出生年份在 1970 年之后的 Person 类,但在实现递归方面非常困难。

我的问题是,在检查了树的第一层和第二层的出生日期后,我不知道如何更深入地递归。

只是想知道是否有人有任何指示。

class Person 
  constructor(name, birth) 
    this.name = name;
    this.birth = birth;
    this.desc = [];
  

  adddesc(person) 
    this.desc.push(person);
  

  post1970() 
    let res = [];
    let birth = this.birth;
    let descendants = this.desc;
    
    if (birth > 1970) return this;

    for (let i = 0; i < descendants.length; i++) 
      let child = descendants[i];
      let curr = child.post1970(); //What should I do after this line?
      console.log(curr)
    
    return res;
  


const original = new Person('root', 1900)

let desc1 = new Person("andrew", 1940);
let desc2 = new Person("sarah", 1960);

let desc3 = new Person("charlie", 1980);
let desc4 = new Person("david", 1990);
let desc5 = new Person("ethan", 2000);

original.adddesc(desc1); //Andrew + Charlie
original.adddesc(desc3);

desc1.adddesc(desc2); //Andrew: Sarah
desc3.adddesc(desc4); //Charlie: David
desc4.adddesc(desc5); //David: Ethan


original.post1970();

【问题讨论】:

我投票决定重新打开,因为问题已经过编辑和改进,并提供了更多细节。但如果它没有重新打开,这里有一种方法:const post1970 = (person) =&gt; [... (person .birth &gt; 1970 ? [person]: []), ... (person .desc) .flatMap (post1970)]。这不是 Person 类的方法,而是一个可以在 Person 实例上运行的简单函数。如果你打电话给post1970 (original) .map (p =&gt; p .name),你应该得到['charlie', 'david', 'ethan'] 【参考方案1】:

我有一些前期的设计建议:

post1970() 是 overly-rigid。年份应该是一个参数。否则,您必须重新编写整个函数才能过滤到 1969 年或任何其他年份。像 bornAfter(year) 这样的合约会是更好的方法,或者进一步概括并使其成为像 filterDescendants(predicateFunction) 这样的任意谓词。 避免使用let,除非您绝对必须使用它。 const 使您的代码更安全、更具表现力。每当我看到let 时,我都觉得程序员在试图告诉我“我打算重新分配这个变量”。在大多数情况下,这是一条红鲱鱼,包括这个——使用const。重新分配是一个拐杖,往往会使代码难以遵循。 desc 让我想到“描述”。 descendants 更清晰。我也将它作为构造函数的数组;如果您也想要 adder/setter/getter,那也可以,但是不允许一次性构建对象并且必须将其存储在中间变量中以逐步设置其属性,这有点尴尬。 在Person 类上具有这些面向树的过滤功能有点奇怪。您可能需要一个具有根 PersonFamilyTree 类。这使得代码读起来像FamilyTree.filterBy(e =&gt; e.birthDate &gt; 1970),而不是不太直观的Person.filterDescendantsBy(e =&gt; e.birthDate &gt; 1970)。 如果您经常进行这样的非分层过滤,那么树数据结构可能是错误的选择。您可以有一个构建一次的辅助数组,或者直接使用该数组而不使用树。如果没有关于您的用例和数据集大小的更多信息,除了注意在每个过滤器操作上遍历整个树感觉很浪费之外,不可能在这里提出建议——对于下面的代码,不需要树抽象一点也不;直接去一个数组并过滤掉。我假设您稍后会添加其他对树实际上有意义的方法。

无论如何,树到数组转换的核心是泛型descendantsToArray,它使用Array#flatMap 递归地构建子数组,然后将它们展平为最终结果数组。一旦你构建了整个树的结果数组,你就可以使用Array#filter 调用来获得你想要的结果。最后,我使用Array#map 来简化输出,这样您就不会看到一堆嵌套的对象。

class Person 
  constructor(name, birthDate, descendants=[]) 
    this.name = name;
    this.birthDate = birthDate;
    this.descendants = descendants;
  
  
  descendantsToArray() 
    return [this].concat(
      ...this.descendants.flatMap(e => e.descendantsToArray())
    );
  

  filterDescendants(predicate) 
    return this.descendantsToArray().filter(predicate);
  


const root = new Person(
  "root",
  1900,
  [
    new Person(
      "andrew",
      1940,
      [new Person("sarah", 1960)]
    ),
    new Person(
      "charlie",
      1980,
      [
        new Person(
          "david",
          1990,
          [new Person("ethan", 1990)]
        )
      ]
    ),
  ],
);
const peopleBornAfter1970 = root
  .filterDescendants(e => e.birthDate > 1970)
  .map(e => e.name)
;
console.log(peopleBornAfter1970);

如果效率是一个大问题,作为第二步的过滤有点浪费,所以您可以尝试generator function 以节省空间。不过整体递归模式是一样的。

【讨论】:

抱歉回复晚了。我对反对票感到沮丧,因此试图自己实施。感谢您的详细回复。 没问题——在编辑之前,人们可能不喜欢 repl 链接,但现在看起来很合题。【参考方案2】:

几个注意事项:

第一,我同意ggorlen的前四点,部分同意第五点。这都是很好的建议,请考虑一下。

第二,如果您正在寻找对现有代码的最小更改,它可能如下所示:

  post1970() 
    let res = [];
    let birth = this.birth;
    let descendants = this.desc;

    // if (birth > 1970) return this; // No, we will need to return an array of people
    if (birth > 1970) res .push (this)
      
    for (let i = 0; i < descendants.length; i++) 
      let child = descendants[i];
      let curr = child.post1970(); // Q: What should I do after this line?
      res = res .concat (curr)     // A: this!  :-)
      // console.log(curr)
    
    return res;
  

(但请注意,这会立即禁止 ggorlen 的 const-over-let 建议,因为我们会在此过程中重新分配 res。这是我个人不喜欢此代码的原因之一。)

第三,我同意 ggorlen 让这段代码更灵活的目标。在这里,我建议使用普通函数而不是类方法。这也可以继续使用普通对象而不是整个树的类实例,但相同的代码将适用于任何一个。以下是我的处理方法:

const treeFilter = (getChildren) => (pred) => (node) => [
  ... (pred (node) ? [node] : []),
  ... (getChildren (node) || []) .flatMap (treeFilter (getChildren) (pred))
]

const familyTreeFilter = treeFilter (n => n .desc)

const post1970 = familyTreeFilter (n => n .birth > 1970)

const original = name: "root", birth: 1900, desc: [name: "andrew", birth: 1940, desc: [name: "sarah", birth: 1960, desc: []], name: "charlie", birth: 1980, desc: [name: "david", birth: 1990, desc: [name: "ethan", birth: 2000,desc: []]]]

console .log (
  post1970 (original) /* for display --> */ .map (n => n .name)
)

我们从treeFilter 开始,它接受一个描述树结构的函数(这里我们的孩子在节点desc [这听起来像是“描述”而不是“后代”],但是可能还有其他可能性,例如children。)这将返回一个接受谓词的函数——为谓词提供一个值,它会报告我们是否要包含该值。它返回一个接受树的函数,并将树的匹配元素作为平面数组返回。

这个 curried 版本的优点是我们可以在任何类似结构的树上重用简单的post1970。我们可以重用familyTreeFilter 为此类descendant-child 结构创建其他过滤器。也许我们想要所有在 2000 年之前拥有birth 的人,或者那些name'c' 开头的人,或者那些至少有四个后代的人。以这种方式编写每一个都是微不足道的。我们可以重用treeFilter 来描述完全不同的树结构上的过滤器,同样,只需简单的实现。

第四,如果这些抽象层次过于庞大,那么很容易将它们组合起来,将它们逐个内联,从而为您的用例获取自定义函数。我很少发现这是必要的,但我可能会在性能密集型代码块上这样做,或者如果我需要以某种简短形式添加函数(比如在 Stack Overflow 帖子上的 a comment。?)但如果你需要,我们可以这样:

const treeFilter = (getChildren) => (pred) => (node) => [
  ... (pred (node) ? [node] : []),
  ... (getChildren (node) || []) .flatMap (treeFilter (getChildren) (pred))
]

const familyTreeFilter = treeFilter (n => n .desc)

const post1970 = familyTreeFilter (n => n .birth > 1970)

和内联familyTreeFilter,把它变成

const treeFilter = (getChildren) => (pred) => (node) => [
  ... (pred (node) ? [node] : []),
  ... (getChildren (node) || []) .flatMap (treeFilter (getChildren) (pred))
]

const post1970 = treeFilter (n => n .desc) (n => n .birth > 1970)

然后我们可以内联treeFilter 来获取

const post1970 = (node) => [
  ... (node .birth > 1970 ? [node] : []),
  ... (node .desc || []) .flatMap (post1970)
]

这段代码本身就更简单了。但是我们之前所做的分解不仅帮助我们找到了这段代码,还为我们提供了更大的灵活性来构建其他工具。

最后,为了完整起见,我们应该证明此代码在您的基于 class 的实现上的工作方式相同。展开这个 sn-p 可以看到:

const treeFilter = (getChildren) => (pred) => (node) => [
  ... (pred (node) ? [node] : []),
  ... (getChildren (node) || []) .flatMap (treeFilter (getChildren) (pred))
]

const familyTreeFilter = treeFilter (n => n .desc)

const post1970 = familyTreeFilter (n => n .birth > 1970)

class Person 
  constructor(name, birth) 
    this.name = name;
    this.birth = birth;
    this.desc = [];
  

  adddesc(person) 
    this.desc.push(person);
  


const original = new Person('root', 1900)

let desc1 = new Person("andrew", 1940);
let desc2 = new Person("sarah", 1960);

let desc3 = new Person("charlie", 1980);
let desc4 = new Person("david", 1990);
let desc5 = new Person("ethan", 2000);

original.adddesc(desc1); //Andrew + Charlie
original.adddesc(desc3);

desc1.adddesc(desc2); //Andrew: Sarah
desc3.adddesc(desc4); //Charlie: David
desc4.adddesc(desc5); //David: Ethan


console .log (
  post1970 (original) /* for display --> */ .map (n => n .name)
)

【讨论】:

以上是关于递归遍历家谱时遇到问题的主要内容,如果未能解决你的问题,请参考以下文章

二叉树前中后序遍历的实现(递归和非递归版)

[数据结构]基于二叉树的家谱系统

遍历列表时遇到问题

遍历数组时遇到问题

帮助我理解中序遍历而不使用递归

在c ++中循环遍历数组时遇到问题[关闭]