如何编写这个递归函数来找到我的对象的最大深度?

Posted

技术标签:

【中文标题】如何编写这个递归函数来找到我的对象的最大深度?【英文标题】:How do I write this recursive function to find the max depth of my object? 【发布时间】:2018-01-29 15:52:28 【问题描述】:

我正在尝试编写一个函数,该函数将遍历我的对象并返回对象的关卡深度。

例如,如果我在这个对象上运行函数:

var test = 
  name: 'item 1',
  children: [
    name: 'level 1 item',
    children: [
      name: 'level 2 item'
    ,
    
      name: 'second level 2 item',
      children: [
        name: 'level 3 item'
      ]
    ]
  ]


var depth = myFunction(test); // Would return 2 (0 index based) as the collection goes 3 levels deep

我一直在尝试编写一个确定最大深度的递归函数,但到目前为止我还无法做到。这是我目前所拥有的:https://jsfiddle.net/6cc6kdaw/2/

返回的值似乎是每个节点命中的计数,而不是唯一级别。我知道我哪里出错了(从某种意义上说我没有过滤掉它),但我已经盯着代码这么久了,以至于没有任何意义了!

谁能指出我哪里出错了?谢谢

【问题讨论】:

请添加minimal, complete, and verifiable example,它在问题本身中显示实际问题/代码,而不仅仅是指向外部资源的链接。 【参考方案1】:

您可以映射每个孩子的深度并取其最大值。

function getDepth(object) 
    return 1 + Math.max(0, ...(object.children || []).map(getDepth));


var test =  name: 'item 1', children: [ name: 'level 1 item', children: [ name: 'level 2 item' ,  name: 'second level 2 item', children: [ name: 'level 3 item' ] ] ] ,
    depth = getDepth(test) - 1; // zero based

console.log(depth);

【讨论】:

你在计算函数中硬编码children吗? @DenysSéguret,是的,children 是嵌套对象的主要数组,看起来像。 好吧,当 OP 希望它更具体时,也许我看到了一个一般性问题 你激励了我:const depth = ( children = [] ) => 1 + Math.max (0, ...children.map(depth))? @naomik,如果没有三元,它会更短:children.length && 1 + Math.max (...children.map (depth))【参考方案2】:

美丽

深度函数是用递归表达得非常漂亮的函数之一——这个答案类似于 Nina 的答案,但说明了不同的推理路线。

const depth = ( children = [] ) =>
  children.length === 0
    ? 0                                      // base
    : 1 + Math.max (...children.map (depth)) // inductive

首先我们解构传入节点,当属性未设置时分配children = []。这使我们可以使用数组的传统基础和归纳案例来解决问题:

基本情况:数组为空 归纳案例:数组为空,因此我们有至少一个元素需要处理

Nina 的回答非常巧妙地避免了任何if 或三元?:!她通过偷偷将基本情况作为Math.max 的第一个参数来做到这一点!她太聪明了

这是一个有效的例子

const depth = ( children = [] ) =>
  children.length === 0
    ? 0
    : 1 + Math.max (...children.map (depth))

const test = 
   name: 'level 0 item'
  , children: 
      [  name: 'level 1 item'
        , children:
            [  name: 'level 2 item' 
            ,  name: 'second level 2 item'
              , children:
                  [  name: 'level 3 item'  ]
              
            ]
        
      ,  name: 'second level 1 item'
        , children:
            [  name: 'level 2 item' 
            ,  name: 'second level 2 item'
              , children: 
                  [  name: 'level 3 item'
                    , children:
                        [  name: 'level 4 item'  ]
                    
                  ]
              
            ]
        
      ]
  

console.log (depth (test))
// 4

野兽

我们在上面使用了一些高级函数和语言实用程序。如果我们是这个概念的新手,在先学会低层次思考之前,我们无法实现更高层次的思考

Math.max 接受任意数量的参数。这究竟是如何工作的? 我们使用剩余参数语法...children 将值数组转换为函数调用中的单个参数。这种转换究竟是如何工作的? 我们使用Array.prototype 中的map 将我们的子节点数组转换为节点深度数组。这是如何运作的?我们真的需要创建一个 new 数组吗?

为了培养对这些内置功能和特性的理解,我们将研究如何自行实现这些结果。我们将重温depth,但这次我们将用我们自己的努力取代所有这些魔法

const depth = ( children = [] ) =>
  children.length === 0
    ? 0                        // base
    : 1 + magicWand (children) // inductive

现在我们只需要一根魔杖……首先,我们从一些基本的制作材料开始

const isEmpty = (xs = []) =>
  xs.length === 0

const first = (xs = []) =>
  xs [0]

const rest = (xs = []) =>
  xs.slice (1)

我想继续思考 baseinductive 的情况,这些原始函数补充了这种推理。

让我们首先了解magicWand 将(必须)如何工作

// magicWand takes a list of nodes and must return a number
1 + magicWand (children)

那么让我们看看我们的两个案例

基本情况:输入列表isEmpty,所以返回0——没有孩子,所以没有要添加的深度 归纳案例:列表有至少一个子项 - 计算 first 项目的深度,在 rest 上挥动魔杖,然后取出 @987654344 @ 这两个值中的一个

我们的魔杖完成了

const magicWand = (list = []) =>
  isEmpty (list)
    // base
    ? 0
    // inductive
    : max ( depth (first (list))
          , magicWand (rest (list))
          )

剩下的就是定义max

const max = (x = 0, y = 0) =>
  x > y
    ? x
    : y

只是为了确保此时一切仍然正常......

const max = (x = 0, y = 0) =>
  x > y
    ? x
    : y

const isEmpty = (xs = []) =>
  xs.length === 0

const first = (xs = []) =>
  xs [0]

const rest = (xs = []) =>
  xs.slice (1)

const depth = ( children = [] ) =>
  children.length === 0
    ? 0                        // base
    : 1 + magicWand (children) // inductive
    
const magicWand = (list = []) =>
  isEmpty (list)
    // base
    ? 0
    // inductive
    : max ( depth (first (list))
          , magicWand (rest (list))
          )

const test = 
   name: 'level 0 item'
  , children: 
      [  name: 'level 1 item'
        , children:
            [  name: 'level 2 item' 
            ,  name: 'second level 2 item'
              , children:
                  [  name: 'level 3 item'  ]
              
            ]
        
      ,  name: 'second level 1 item'
        , children:
            [  name: 'level 2 item' 
            ,  name: 'second level 2 item'
              , children: 
                  [  name: 'level 3 item'
                    , children:
                        [  name: 'level 4 item'  ]
                    
                  ]
              
            ]
        
      ]
  

console.log (depth (test)) // 4

所以要实现更高层次的思考,你必须首先想象你的程序运行时会发生什么

const someList =
  [ x, y, z ]

magicWand (someList)
// ???

xyz 是什么并不重要。您只需想象magicWand 将使用每个独立部分构建的函数调用堆栈。我们可以看到随着更多项目添加到输入列表中,这将如何扩展......

max ( depth (x)
    , max ( depth (y)
          , max ( depth (z)
                , 0
                )
          )
    )

当我们看到我们的函数构建的计算时,我们开始看到它们结构的相似之处。当一个模式出现时,我们可以在一个可重用的函数中捕捉到它的本质

在上面的计算中,maxmagicWand 被硬编码到我们的程序中。如果我想用树计算不同的值,我需要一根完全不同的魔杖。

这个函数被称为 fold 因为它在一个可遍历的数据结构中的每个元素之间折叠一个用户提供的函数f。您将看到我们的标志性基础和归纳案例

const fold = (f, base, list) =>
  isEmpty (list)
    ? base
    : f ( fold ( f
               , base
               , rest (list)
               )
        , first (list)
        )

现在我们可以使用我们的通用 fold 重写 magicWand

const magicWand = (list = []) =>
  fold ( (acc, x) => max (acc, depth (x))
       , 0
       , list
       )

magicWand 抽象不再需要。 fold可以直接在我们原来的函数中使用。

const depth = ( children = [] ) =>
  children.length === 0
    ? 0
    : 1 + fold ( (acc, x) => max (acc, depth (x))
               , 0
               , children
               )

当然,阅读原文要困难得多。语法糖为您提供了代码中的各种快捷方式。缺点是初学者经常觉得必须总是有某种甜蜜的"shorthand" 解决他们的问题 - 然后他们被困when it's just not there。

功能代码示例

const depth = ( children = [] ) =>
  isEmpty (children)
    ? 0
    : 1 + fold ( (acc, x) => max (acc, depth (x))
               , 0
               , children
               )

const fold = (f, base, list) =>
  isEmpty (list)
    ? base
    : f ( fold ( f
               , base
               , rest (list)
               )
        , first (list)
        )
        
const max = (x = 0, y = 0) =>
  x > y
    ? x
    : y

const isEmpty = (xs = []) =>
  xs.length === 0
  
const first = (xs = []) =>
  xs [0]
  
const rest = (xs = []) =>
  xs.slice (1)

const test = 
   name: 'level 0 item'
  , children: 
      [  name: 'level 1 item'
        , children:
            [  name: 'level 2 item' 
            ,  name: 'second level 2 item'
              , children:
                  [  name: 'level 3 item'  ]
              
            ]
        
      ,  name: 'second level 1 item'
        , children:
            [  name: 'level 2 item' 
            ,  name: 'second level 2 item'
              , children: 
                  [  name: 'level 3 item'
                    , children:
                        [  name: 'level 4 item'  ]
                    
                  ]
              
            ]
        
      ]
  

console.log (depth (test))
// 4

野兽模式

depth 的最后一个实现中,我们看到了这个 lambda(匿名函数)表达式。

(acc, x) => max (acc, depth (x))

我们即将见证我们自己创造的一项令人难以置信的发明。这个小 lambda 非常有用,我们实际上会给它一个名字,但在我们能够利用它的真正力量之前,我们必须首先制作 maxdepth 参数——我们已经制作了一个新的魔杖

const magicWand2 = (f, g) =>
  (acc, x) => g (acc, f (x))

const depth = ( children = [] ) =>
  isEmpty (children)
    ? 0
    : 1 + fold (magicWand2 (depth, max), 0, children)

// Tada!

乍一看,你认为这一定是有史以来最没用的魔杖!你可能会怀疑我是那些在一切都变成point-free 之前不会停止的僵尸之一。你吸气并暂停你的反应片刻

const concat = (xs, ys) =>
  xs.concat (ys)

const map = (f, list) =>
  fold (magicWand2 (f, concat), [], list)

map (x => x * x, [ 1, 2, 3, 4 ])
// => [ 16, 9, 4, 1 ]

诚然,我们认为这很酷。但我们不会被 2 把戏的小马眼花缭乱。明智的做法是阻止任何旧函数登陆您的程序或库,但如果忽略这一点,您将是个傻瓜。

const filter = (f, list) =>
  fold ( magicWand2 (x => f (x) ? [ x ] : [], concat)
       , []
       , list
       )

filter (x => x > 2, [ 1, 2, 3, 4 ])
// [ 4, 3 ]

好的,除了mapfilter 以“相反”的顺序组装结果之外,这根魔杖有一些严重的热量。我们称它为mapReduce,因为它给了我们两个参数,每个参数一个函数,并创建一个新的归约函数来插入fold

const mapReduce => (m, r) =>
  (acc, x) => r (acc, m (x))
    mmapping 函数——这让你有机会在...之前转换传入的元素。 rreducing 函数 - 该函数将累加器与映射元素的结果相结合

至于fold 在“反向”中组装结果,它不是。这恰好是一个右折叠。下面,我们可以将f 想象成一些二进制函数(即+)——参见前缀符号f (x y) 中的计算,而中缀符号x + y 应该有助于突出关键区别

foldR (f, base, [ x, y, z ])
// = f (f (f (base, z), y), x)
// = ((base + z) + y) + x

foldL (f, base, [ x, y, z ])
// = f (f (f (base, x), y), z)
// = (((base + x) + y) + z

现在让我们定义我们的左折叠,foldL - 我将fold 重命名为foldR 并将它放在这里,以便我们可以并排看到它们。

const foldL = (f, base, list) =>
  isEmpty (list)
    ? base
    : foldL ( f
            , f (base, first (list))
            , rest (list)
            )

const foldR = (f, base, list) =>
  isEmpty (list)
    ? base
    : f ( foldR ( f
               , base
               , rest (list)
               )
        , first (list)
        )

许多 javascript 开发人员不知道 reduceRight 存在于 Array.prototype 上。如果您只使用过 reduce 的交换函数,您将无法检测到差异。

好的,所以要修复我们的 mapfilter,我们只需将 fold 绑定替换为 foldL

const map = (f, list) =>
  foldL (mapReduce (f, concat), [], list)

const filter = (f, list) =>
  foldL (mapReduce (x => f (x) ? [ x ] : [], concat), [], list)

const square = x =>
  x * x

const gt = x => y =>
  y > x

map (square, filter (gt (2), [ 1, 2, 3, 4 ]))
// => [ 9, 16 ]

使用我们自己的map,我们可以重写depth,使其更接近我们的原始形式...

const depth = ( children = [] ) =>
  isEmpty (children)
    ? 0
    : 1 + foldL ( max
                , 0
                , map (depth, children)
                )

但我希望我们停下来想想为什么这实际上比直接使用mapReducedepth 更糟糕...

够了

让我们花点时间想想我们在map-filter 示例中做了什么。 filter 遍历我们的整个输入数组,为每个数字调用gt (2),产生[ 3, 4 ] 的中间结果。 然后 map 调用 squarefor 中间结果中的数字,产生最终值 [ 9, 16 ]。数据变大了,我们希望看到这样的代码:

myBigData.map(f).map(g).filter(h).map(i).map(j).reduce(k, base)

mapReduce 拥有一种会腐蚀其旁观者的力量。你以为我是心甘情愿写这个回答,其实我只是mapReduce的俘虏!这种结构是一些社区称之为transducers的东西的核心——恰好是a subject I've written about here on SO。我们开发了一种 折叠的折叠 直觉——就像魔术一样,用尽的多个循环折叠成一个折叠。如果您对此主题感兴趣,我鼓励您进一步阅读!

【讨论】:

【参考方案3】:

这是一个建议:

function depth(o)
  var values;
  if (Array.isArray(o)) values = o;
  else if (typeof o === "object") values = Object.keys(o).map(k=>o[k]);
  return values ? Math.max.apply(0, values.map(depth))+1 : 1;

var test = 
  name: 'item 1',
  children: [
    name: 'level 1 item',
    children: [
      name: 'level 2 item'
    ,
    
      name: 'second level 2 item',
      children: [
        name: 'level 3 item'
      ]
    ]
  ]
;

function depth(o)
  var values;
  if (Array.isArray(o)) values = o;
  else if (typeof o === "object") values = Object.keys(o).map(k=>o[k]);
  return values ? Math.max.apply(0, values.map(v=>depth(v)))+1 : 1;


console.log(depth(test))

编辑:这是一个通用的解决方案。我仍然不确定您是否想要一个非常具体的字段(即带有 children 字段或通用字段)。

【讨论】:

非常好的答案。一个非常小的建议:values.map(v=>depth(v))values.map(depth)【参考方案4】:

我对您的代码进行了一些更改,以便按照您的意愿工作:

function getDepth(node, depth = 0) 
    if (!!node.children && node.children.length > 0) 
        var childDepths = [];
        depth++;

        for (var i = 0; i < node.children.length; i++) 
            childDepths.push(getDepth(node.children[i]));
        
        return depth + Math.max(...childDepths);
    

    return depth;


var depth = getDepth(root);

console.log('currentMaxDepth', depth);
console.log('root', root);

诀窍是测量同一级别的兄弟姐妹的深度,然后选择其中最深的。这里还有一个 jsfiddle:https://jsfiddle.net/2xk7zn00/

【讨论】:

【参考方案5】:

在函数中

function getMaxDepth(root) 
    var currentMaxDepth = 0;

    return getDepth(currentMaxDepth, root);

您使用 root 调用 getDepth,但 root 永远不会更改。所以它总是一样的。例如,你应该打电话,

return getDepth(currentMaxDepth, root.children[0]);

【讨论】:

【参考方案6】:

警告..!

只是为了好玩..!

这是一个利用 Y 组合子现象的解决方案。我们将利用递归的匿名函数来解决问题。

匿名函数如下;

d => (o, n = 0)  => o.children ? o.children.reduce(function(r,e)
                                                     var z = d(e,n+1);
                                                     return n > z ? n : z;
                                                   , n)
                               : n;

有一个神奇的(也是不存在的)函数d 可以找到物体的深度。如果我们使用d 提供匿名函数,它将返回一个函数,我们可以使用该函数提供对象并找到它的深度。

让我们开始吧; :))

var Y  = f => (g => g(g))(g => (o,z) => f(g(g))(o,z)),
    fd = Y(d => (o, n = 0)  => o.children ? o.children.reduce(function(r,e)
                                                                var z = d(e,n+1);
                                                                return n > z ? n : z;
                                                              , n)
                                          : n),
    o  =  name: 'item 1', children: [ name: 'level 1 item', children: [ name: 'level 2 item' ,  name: 'second level 2 item', children: [ name: 'level 3 item' ] ] ] ,
    r  = fd(o);
console.log(r);

如果您想了解更多关于 Y 组合子的信息,您可以阅读this。

【讨论】:

以上是关于如何编写这个递归函数来找到我的对象的最大深度?的主要内容,如果未能解决你的问题,请参考以下文章

递归函数

递归函数

如何将递归函数转换为使用堆栈?

RecursionError:超出最大递归深度

Python中的最大递归深度是多少,如何增加它?

为啥 Python 有最大递归深度?