如何编写此递归函数以查找对象的最大深度?

时间:2018-01-29 15:52:29

标签: javascript recursion

我正在尝试编写一个循环访问我的对象并返回对象的层次深度的函数。

例如,如果我在此对象上运行该函数:

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/

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

任何人都能指出我哪里出错了?感谢

6 个答案:

答案 0 :(得分:7)

<强>美容

深度函数是递归表达得非常漂亮的函数之一 - 这个答案与Nina相似,但说明了不同的推理方式。

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

首先我们对传入节点进行解构,在没有设置属性时分配children = []。这允许我们使用传统的基数和集成的归纳案例来解决问题:

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

Nina的回答非常巧妙地完全避免了任何if或三元?:!她通过将基础案例作为Math.max的第一个参数偷偷摸摸地做到了这一点!她很聪明&lt; 3

这是一个有效的例子

&#13;
&#13;
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
&#13;
&#13;
&#13;

野兽

我们使用了上面的一些高级功能和语言实用程序。如果我们对这个概念不熟悉,那么在第一次学习在较低层次思考之前,我们就无法达到更高层次的思维

  • Math.max接受任意数量的参数。这是如何工作的?
  • 我们使用rest参数语法...children将值数组转换为函数调用中的单个参数。这种转换如何正常工作?
  • 我们使用map中的Array.prototype将我们的子节点数组转换为节点深度数组。这是如何运作的?我们真的需要制作 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)

我想继续考虑 base inductive 案例,这些原始函数与这条推理路线相辅相成。

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

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

让我们来看看我们的两个案例

  • 基本情况:输入列表isEmpty,所以返回0 - 没有子节点,因此没有要添加的深度
  • 归纳案例:列表中有至少一个子项 - 计算first项的深度,在{{1}上挥动魔杖},并获取这两个值的rest

我们的魔杖是完整的

max

剩下的就是定义const magicWand = (list = []) => isEmpty (list) // base ? 0 // inductive : max ( depth (first (list)) , magicWand (rest (list)) )

max

只是为了确保此时所有内容仍在工作......

&#13;
&#13;
const max = (x = 0, y = 0) =>
  x > y
    ? x
    : y
&#13;
&#13;
&#13;

因此,为了实现更高层次的思考,您必须首先想象当您的程序运行时会发生什么

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) // ??? xy是什么并不重要。您只需要想象z将使用每个独立部分构建的函数调用堆栈。随着更多项目添加到输入列表中,我们可以看到这会如何扩展...

magicWand

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

在上面的计算中,max ( depth (x) , max ( depth (y) , max ( depth (z) , 0 ) ) ) max被硬编码到我们的程序中。如果我想用树计算不同的值,我需要一个完全不同的魔杖。

此函数称为 fold ,因为它在可遍历数据结构中的每个元素之间折叠用户提供的函数magicWand。您将看到我们的签名基础和归纳案例

f

现在我们可以使用通用const fold = (f, base, list) => isEmpty (list) ? base : f ( fold ( f , base , rest (list) ) , first (list) )

重写magicWand
fold

不再需要const magicWand = (list = []) => fold ( (acc, x) => max (acc, depth (x)) , 0 , list ) 抽象。 magicWand可以直接用于我们的原始函数。

fold

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

功能代码示例

&#13;
&#13;
const depth = ({ children = [] }) =>
  children.length === 0
    ? 0
    : 1 + fold ( (acc, x) => max (acc, depth (x))
               , 0
               , children
               )
&#13;
&#13;
&#13;

野兽模式

潜伏在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的最后一个实现中,我们看到了这个lambda(匿名函数)表达式。

depth

我们即将见证我们自己制造的令人难以置信的发明。这个小lambda是如此有用,我们实际上给它起了一个名字,但在我们能够利用它的真正力量之前,我们必须首先制作(acc, x) => max (acc, depth (x)) max参数 - 我们已经制作了一个新魔杖

depth
乍一看,你认为这一定是最无用的魔杖!你可能会怀疑我是其中一个僵尸,在一切都是point-free之前不会停止。你暂时吸气并暂停你的反应

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

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

// Tada!

不可否认,我们认为这很酷。但是我们不会被一个2招小马弄得眼花缭乱。你可以明智地防止任何旧功能登陆你的程序或图书馆,但忽略这一功能你是个傻瓜。

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 ]

好的,除了const filter = (f, list) => fold ( magicWand2 (x => f (x) ? [ x ] : [], concat) , [] , list ) filter (x => x > 2, [ 1, 2, 3, 4 ]) // [ 4, 3 ] map正在将结果汇总在&#34;反向&#34;为了这个魔杖有一些严重的热量。我们称之为filter,因为它为我们提供了两个参数,每个参数都是一个函数,并创建一个新的缩减函数以插入mapReduce

fold
  1. const mapReduce => (m, r) => (acc, x) => r (acc, m (x)) 映射函数 - 这使您有机会在...之前转换传入元素。
  2. m reduce 函数 - 此函数将累加器与映射元素的结果相结合
  3. 至于r将结果汇总在&#34;反向&#34;中,它不是。这恰好是右折。下面,我们可以将fold想象成一些二元函数(即f) - 请参阅前缀表示法+中的计算,中缀表示法f (x y)应该有助于突出显示关键区别< / p>

    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

    许多JavaScript开发人员都不知道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) ) 上存在reduceRight。如果您只使用Array.prototype的交换功能,则无法检测到差异。

    好的,为了修复我们的reducemap,我们只需将filter绑定替换为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) ) 的{​​{1}}更糟糕...

    够了

    让我们花点时间思考一下depth - mapReduce示例我们在那里做了些什么。 map遍历整个输入数组,为每个数字调用filter,产生filter的中间结果。 然后 gt (2)调用[ 3, 4 ]作为中间结果中的数字,产生最终值map。数据变得很大,我们希望看到这样的代码:

    square

    [ 9, 16 ]具有破坏其旁观者的那种力量。你觉得我心甘情愿地写这个答案,但我只是myBigData.map(f).map(g).filter(h).map(i).map(j).reduce(k, base) 的囚徒!这种结构是某些社区称之为传感器的核心 - 恰好是a subject I've written about here on SO。我们开发了折叠折叠直觉 - 就像魔法一样,耗尽多个循环会折叠成一个折叠。如果您对该主题感兴趣,我建议您进一步阅读!

答案 1 :(得分:5)

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

&#13;
&#13;
$ head out[12]
==> out1 <==
bla bla bla ABCDEF blabla GHIJKL.
bla bla bla bla bla.

==> out2 <==
bla bla bla MNOPQR blabla STUVWX.
bla bla bla bla bla.
&#13;
&#13;
&#13;

答案 2 :(得分:4)

这是一个提案:

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字段或通用字段。)

答案 3 :(得分:1)

在功能

function getMaxDepth(root) {
    var currentMaxDepth = 0;

    return getDepth(currentMaxDepth, root);
}

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

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

答案 4 :(得分:1)

我已根据您的需要更改了您的代码:

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 :(得分:1)

警告..!

只是为了好玩..!

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

匿名功能如下;

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提供匿名函数,它将返回一个函数,我们可以使用该函数提供对象并找到它的深度。

所以我们这样做; :))

&#13;
&#13;
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);
&#13;
&#13;
&#13;

如果您想了解有关 Y组合器的更多信息,请阅读this