【问题标题】:Hierarchical Tree from Structured Array of Dependent Elements依赖元素结构化数组的层次树
【发布时间】:2021-08-24 01:20:39
【问题描述】:

我正在寻找一种有效的方法来从 JavaScript 中的以下数据结构构建树。每个条目的长度始终为 5,并且条目之间永远不存在任何间隙(即空值)。

var geographies = [
    [ 'Denmark', 'Midtjylland', null, null, null ],
    [ 'Denmark', 'Syddanmark', 'Langeland', null, null ],
    [ 'Denmark', 'Syddanmark', 'Ærø', null, null ],
    [ 'Japan', 'Okinawa', 'Izenajima', null, null ],
    [ 'Japan', 'Hokkaido', 'Rishiri', 'Rishiri-to', null ]
]

所需的输出应如下所示:

[{
    label: "Denmark",
    children: [{
            label: "Midtjylland",
        },
        {
            label: "Syddanmark",
            children: [{
                label: "Langeland"
            },
            {
                label: "Ærø"
            }]
        }
    ]
}, {
    label: "Japan",
    children: [{
        label: "Okinawa",
        children: [{
            label: "Izenajima"
        }]
    }, {
        label: "Hokkaido",
        children: [{
            label: "Rishiri",
            children: [{
                label: 'Rishiri-to'
            }]
        }]
    }]
}]

【问题讨论】:

    标签: javascript recursion tree hierarchy hierarchical-data


    【解决方案1】:

    可以在每个子数组的第一个元素上对数组进行分组,如果分组数组不为空,则调用to_tree

    function to_tree(d){
       var c = {}
       for (var i of d){
           var k = i.shift();
           c[k] = k in c ? [...c[k], i] : [i]
       }
       return Object.keys(c).map(function(x){return {label:x, ...(c[x].filter(j => j.length).length ? {children:to_tree(c[x].filter(j => j.length))} : {})}})
    }
    var geographies = [[ 'Denmark', 'Midtjylland', null, null, null ], [ 'Denmark', 'Syddanmark', 'Langeland', null, null ], [ 'Denmark', 'Syddanmark', 'Ærø', null, null ], [ 'Japan', 'Okinawa', 'Izenajima', null, null ], [ 'Japan', 'Hokkaido', 'Rishiri', 'Rishiri-to', null ]]
    var geographies1 = [[ 'Greece', null, null, null, null ], [ 'Greece', 'Ionian Islands', 'Lefkada', null, null ], [ 'Greece', 'Attica', 'Salamis', null, null ], [ 'Greece', 'Ionian Islands', 'Cephalonia', null, null ], [ 'Greece', 'Thessaly', 'Skiathos', null, null ], [ 'Greece', 'South Aegean', 'Kea', null, null ], [ 'Greece', 'Attica', 'Kythira', null, null ]]
    var r = to_tree(geographies.map(x => x.filter(y => y != null)));
    var r1 = to_tree(geographies1.map(x => x.filter(y => y != null)));
    console.log(r)
    console.log(r1)

    输出:

    [
      {
        "label": "Denmark",
        "children": [
          {
            "label": "Midtjylland"
          },
          {
            "label": "Syddanmark",
            "children": [
              {
                "label": "Langeland"
              },
              {
                "label": "Ærø"
              }
            ]
          }
        ]
      },
      {
        "label": "Japan",
        "children": [
          {
            "label": "Okinawa",
            "children": [
              {
                "label": "Izenajima"
              }
            ]
          },
          {
            "label": "Hokkaido",
            "children": [
              {
                "label": "Rishiri",
                "children": [
                  {
                    "label": "Rishiri-to"
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
    
    [
      {
        "label": "Greece",
        "children": [
          {
            "label": "Ionian Islands",
            "children": [
              {
                "label": "Lefkada"
              },
              {
                "label": "Cephalonia"
              }
            ]
          },
          {
            "label": "Attica",
            "children": [
              {
                "label": "Salamis"
              },
              {
                "label": "Kythira"
              }
            ]
          },
          {
            "label": "Thessaly",
            "children": [
              {
                "label": "Skiathos"
              }
            ]
          },
          {
            "label": "South Aegean",
            "children": [
              {
                "label": "Kea"
              }
            ]
          }
        ]
      }
    ]
    

    【讨论】:

    • 我不是反对者,但我会考虑将其作为仅代码的答案。 (“你可以使用递归”不是解释。)
    • 对我来说看起来更好。即使有很多代码,我也倾向于更多文本繁重的答案。正如我所说,我不是反对者,但我会尝试取消它!
    • @ScottSauyet 谢谢。我认为我添加的解释应该对其他读者有所帮助。在前导子数组值上分组的递归模式,然后将递归应用于分组子数组在 SO 上通常用于此类问题,所以我不会说我的解决方案过于新颖 :)
    • 问候@Ajax1234 - 非常感谢。到目前为止,有一种情况是行不通的,即var geographies = [[ 'Greece', null, null, null, null ], [ 'Greece', 'Ionian Islands', 'Lefkada', null, null ], [ 'Greece', 'Attica', 'Salamis', null, null ], [ 'Greece', 'Ionian Islands', 'Cephalonia', null, null ], [ 'Greece', 'Thessaly', 'Skiathos', null, null ], [ 'Greece', 'South Aegean', 'Kea', null, null ], [ 'Greece', 'Attica', 'Kythira', null, null ]] - 这只会在树中产生一个元素 - 如果它也能处理这种情况,我们很乐意接受你的回答
    • 谢谢,这现在可以工作了@Ajax1234!
    【解决方案2】:

    此版本进行双重转换,首先以这种形式递归嵌套您的数据:

    {
        "Denmark": {
            "Midtjylland": {},
            "Syddanmark": {
                "Langeland": {},
                "Ærø": {}
            }
        },
        "Japan": {
            "Okinawa": {
                "Izenajima": {}
            },
            "Hokkaido": {
                "Rishiri": {
                    "Rishiri-to": {}
                }
            }
        }
    }
    

    然后递归地将其转换为您请求的输出。

    没有根本原因不能一次性完成。但我经常发现需要类似中间格式的东西,因此有一些工具可以进行这种转换。从那里开始,最终的转换非常简单。

    // utility functions
    const setPath = ([p, ...ps]) => (v) => (o) =>
      p == undefined ? v : Object .assign (
        Array .isArray (o) || Number .isInteger (p) ? [] : {},
        {...o, [p]: setPath (ps) (v) ((o || {}) [p])}
      )
    
    const nest = (xs) =>
      xs .reduce ((o, p) => setPath (p .filter (Boolean)) ({}) (o), {})
    
    
    // helper function
    const toObjectTree = (o) =>
      Object .entries (o) .map (([name, kids]) => ({
        label: name,
        ...(Object .keys (kids) .length ? {children: toObjectTree (kids)} : {})
      }))
    
    
    // main function
    const convert = (xs) => 
      toObjectTree (nest (xs))
    
    
    // sample data
    const geographies1 = [['Denmark', 'Midtjylland', null, null, null], ['Denmark', 'Syddanmark', 'Langeland', null, null], ['Denmark', 'Syddanmark', 'Ærø', null, null], ['Japan', 'Okinawa', 'Izenajima', null, null], ['Japan', 'Hokkaido', 'Rishiri', 'Rishiri-to', null]]
    const geographies2 = [[ 'Greece', null, null, null, null ], [ 'Greece', 'Ionian Islands', 'Lefkada', null, null ], [ 'Greece', 'Attica', 'Salamis', null, null ], [ 'Greece', 'Ionian Islands', 'Cephalonia', null, null ], [ 'Greece', 'Thessaly', 'Skiathos', null, null ], [ 'Greece', 'South Aegean', 'Kea', null, null ], [ 'Greece', 'Attica', 'Kythira', null, null ]]
    const geographies3 = [[ 'Denmark', 'Syddanmark', 'Langeland', null, null ], [ 'Denmark', 'Syddanmark', null, null, null ], [ 'Denmark', 'Syddanmark', 'Ærø', null, null ], [ 'Japan', 'Okinawa', 'Izenajima', null, null ], [ 'Japan', 'Hokkaido', 'Rishiri', 'Rishiri-to', null ]]
    
    
    // demo
    console .log (convert (geographies1))
    console .log (convert (geographies2))
    console .log (convert (geographies3))
    .as-console-wrapper {max-height: 100% !important; top: 0}

    我们从实用函数 setPath 开始,您可以在我的一些 other answers 中找到更详细的描述。不过,简而言之,它使用字符串和/或整数数组沿所提供对象(的副本)中的嵌套路径设置值。

    然后 nest 从这样的路径数组构建一个对象,叶子节点是空对象。

    我们编写了一个辅助函数 toObjectTree 以递归地将其转换为您的 label/children 结构。

    最后是主函数convert,组合toObjectTreenest进行完整的转换。

    请注意,这里不需要用nulls 填充的固定长度数组。它通过简单地过滤掉任何空值来与它们一起工作。 (filter (Boolean) 很简单,但如果这些路径可能是错误的——可能是空字符串——那么我们必须用filter (x => x!= null) 或类似的东西替换它。)虽然这使它比你的请求更通用,但它应该仍然适用于您的数据。


    对我来说,这是一种非常强大的数据转换方式:采取多个步骤,每一步都将前一个步骤转换为朝着最终目标前进的有用中间格式,然后通过这一系列更简单的转换简单地传输数据。

    【讨论】:

    • 嗨@Scott - 谢谢你非常详细的回答。当我看到这个的时候,我已经研究了第一个试图理解的东西。为了公平对待 Ajax,我已经接受了他的方法,因为我已经在我的解决方案中实现了它,但是你的解释方式非常值得称赞。
    • 大多数在这里工作了一段时间的人都在这样做,因为他们发现问题很有趣。我当然不追逐评级点或任何他们所谓的东西。很高兴您感谢我的回答。
    猜你喜欢
    • 2012-08-11
    • 1970-01-01
    • 1970-01-01
    • 2019-02-03
    • 1970-01-01
    • 2016-07-17
    • 1970-01-01
    • 2013-01-22
    • 1970-01-01
    相关资源
    最近更新 更多