JavaScript递归函数用于数组中嵌套对象的处理

3
我正在尝试实现一个算法来生成一个具有分层标题的表格。这些标题可以无限嵌套。 一个HTML渲染表格标记的示例可能如下所示:

    <table border=1>
      <thead>
        <tr>
          <th colspan="6">
            Super one
          </th>
          <th colspan="6">
            Super two
          </th>
        </tr>
        <tr>
          <th colspan="3">Head one</th>
          <th colspan="3">Head two</th>
          <th colspan="4">Head three</th>
          <th colspan="2">Head four</th>
        </tr>
        <tr>
          <th>Sub one</th>
          <th>Sub two</th>
          <th>Sub three</th>
          <th>Sub four</th>
          <th>Sub five</th>
          <th>Sub six</th>
          <th>Sub seven</th>
          <th>Sub eight</th>
          <th>Sub nine</th>
          <th>Sub ten</th>
          <th>Sub eleven</th>
          <th>Sub twelve</th>
        </tr>
      </thead>
    </table>  

标签的配置应当以以下JavaScript对象格式传递:
var columns = [
  {
    label: 'Super one',
    children: [
      {
        label: 'Head one',
        children: [
          {label: 'Sub one'},
          {label: 'Sub two'},
          {label: 'Sub three'}
        ]
      },
      {
        label: 'Head two',
        children: [
          {label: 'Sub four'},
          {label: 'Sub five'},
          {label: 'Sub six'}
        ]
      }
    ]
  },
  {
    label: 'Super two',
    children: [
      {
        label: 'Head three',
        children: [
          {label: 'Sub seven'},
          {label: 'Sub eight'},
          {label: 'Sub nine'},
          {label: 'Sub ten'}
        ]
      },
      {
        label: 'Head four',
        children: [
          {label: 'Sub eleven'},
          {label: 'Sub twelve'}
        ]
      }
    ]
  }
];

现在,让我们忘记HTML渲染,只关注应该迭代配置的算法,以便以以下格式获得简单的2D数组:

var structure = [
  [6, 6],
  [3, 3, 4, 2],
  [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
]; 

每个条目表示一个包含其列定义(td)的表行(tr),数字表示 colspan

我该如何实现算法?

目前,我创建了一个递归函数,根据配置返回总列数:

function getColumnCount(columns) {
  var count = 0;
  for (var i=0; i<columns.length; i++) {
    var col = columns[i];
    if (col.children && col.children.length > 0) {
      count += getColumnCount(col.children);
    }
    else {
      count++;
    }
  }
  return count;
}

它按预期工作,但我在尝试生成“结构”数组时卡住了... 我当前(令人尴尬的)代码尝试如下:

function getStructure(columns) {
  var structure = [[]];
  for (var i=0; i<columns.length; i++) {
    var col = columns[i];
    if (col.children && col.children.length > 0) {
      console.log(col.label, '(with children)');
      schema[structure.length - 1].push(getColumnCount(col.children));
      getStructure(col.children, schema);
    }
    else {
      console.log(col.label, '(orphan)');
      schema[structure.length - 1].push(1);
    }
  }
  return structure; 
}

我感觉很蠢,因为我知道这应该是一个相对简单的任务,但当涉及到递归函数时,我的大脑似乎拒绝合作 XD

你能帮助我吗?


所有配置中的兄弟节点都具有相同的深度吗? - CFrei
我猜你的意思是如果它们有相同的深度...在这种情况下,好问题...我应该能够处理不同的深度...例如,“Head four”可以配置为没有“Sub eleven”和“Sub twelve”。 - daveoncode
你能给我一个关于“结构体”应该长什么样子的例子吗? - CFrei
3个回答

3

难点在于计算一个范围,它是给定节点下叶节点的数量或者如果节点本身就是叶子,则为1。这个值可以递归地定义如下:

 numberOfLeaves(node) = if node.children then 
       sum(numberOfLeaves(child) for child in node.children) 
       else 1

其余部分非常简单:

var columns = [
    {
        label: 'Super one',
        children: [
            {
                label: 'Head one',
                children: [
                    {
                        label: 'Sub one',
                        children: [
                            {label: 1},
                            {label: 2},
                        ]
                    },
                    {label: 'Sub two'},
                    {label: 'Sub three'}
                ]
            },
            {
                label: 'Head two',
                children: [
                    {label: 'Sub four'},
                    {label: 'Sub five'},
                    {label: 'Sub six'}
                ]
            }
        ]
    },
    {
        label: 'Super two',
        children: [
            {
                label: 'Head three',
                children: [
                    {label: 'Sub seven'},
                    {label: 'Sub eight'},
                    {label: 'Sub nine'},
                    {label: 'Sub ten'}
                ]
            },
            {
                label: 'Head four',
                children: [
                    {label: 'Sub eleven'},
                    {label: 'Sub twelve'}
                ]
            }
        ]
    }
];

var tab = [];

function calc(nodes, level) {
    tab[level] = tab[level] || [];

    var total = 0;

    nodes.forEach(node => {
        var ccount = 0;
        if ('children' in node) {
            ccount = calc(node.children, level + 1);
        } else {
            ccount = 1;
        }
        tab[level].push({
            label: node.label,
            span: ccount
        });

        total += ccount;
    });

    return total;
}

calc(columns, 0);
console.log(tab);

function makeTable(tab) {
    html = "<table border=1>";
    tab.forEach(row => {
        html += "<tr>";
        row.forEach(cell => {
            html += "<td colspan=" + cell.span + ">" + cell.label + "</td>"
        });
        html += "</tr>"
    })
    return html + "</table>";
}

document.write(makeTable(tab))


如果您在下面添加更多子元素而没有在上面添加它们,那么这会起作用吗?比如,在“sub eleven”下面添加了子元素。我认为它不会添加必要的填充... - IMTheNachoMan
@IMTheNachoMan:对于不平衡树,这个很有效 - 请参见上面的“子一”。 - georg
@georg: 将{label: 'Sub twelve'}改为{label: 'Sub twelve', children: [{label: 'dingo'}]}并且它不会起作用。 - IMTheNachoMan
@georg:使用我下面答案中的数据与你的解决方案进行比较,并与我的输出进行对比。 - IMTheNachoMan
@georg:如果树的“右”侧比起点/左侧有更多的子节点,则必须为这些空子节点添加填充内容... - IMTheNachoMan

2

这里是另一种稍微小一点的方法。

function getStructure(nodes) {
    if (nodes.length == 0) { return [ [ 1 ] ] }
    let level1 = nodes.map(node => getStructure(node.children ? node.children: []))
    let ret = level1.reduce((obj, e) => e.map((a, i) => obj[i].concat(a)))
    let sum = ret[0].reduce((sum, e) => sum + e, 0)
    return [ [ sum ] ].concat(ret)
}

生产

[ [ 12 ],
  [ 6, 6 ],
  [ 3, 3, 4, 2 ],
  [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ] ]

我不确定如何处理“不同高度”特性...结构应该是什么样子?

1

这应该适用于任何组合:

var columns = [
    {
        label: '1',
        children: [
            {
                label: '1.1',
                children: [
                    {label: '1.1.1'},
                    {
      label: '1.1.2',
      children: [
       {label: '1.1.2.1'},
       {label: '1.1.2.2'},
       {label: '1.1.2.3'},
       {label: '1.1.2.4'},
       {label: '1.1.2.5'}
      ]
     },
                    {label: '1.1.3'}
                ]
            },
            {
                label: '1.2',
                children: [
                    {label: '1.2.1'},
                    {label: '1.2.2'},
                    {label: '1.2.3'}
                ]
            }
        ]
    },
    {
        label: '2',
        children: [
            {
                label: '2.1',
                children: [
                    {label: '2.1.1'},
                    {label: '2.1.2'},
                    {label: '2.1.3'},
                    {
      label: '2.1.4',
      children: [
       {label: '2.1.4.1'},
       {label: '2.1.4.2'},
       {
        label: '2.1.4.3',
        children: [
         {label: '2.1.4.3.1'},
         {label: '2.1.4.3.2'},
         {
          label: '2.1.4.3.3',
          children: [
           {label: '2.1.4.3.3.1'},
           {label: '2.1.4.3.3.2'},
           {label: '2.1.4.3.3.3'},
           {label: '2.1.4.3.3.4'}
          ]
         },
         {label: '2.1.4.3.4'},
         {label: '2.1.4.3.5'}
        ]
       },
      ]
     }
                ]
            },
            {
                label: '2.2',
                children: [
                    {label: '2.2.1'},
                    {
      label: '2.2.2',
      children: [
       {label: '2.2.2.1'},
       {label: '2.2.2.2'},
      ]
     }
                ]
            }
        ]
    }
];

// table is the table
// cells is the array of cells we're currently processing
// rowIndex is the table row we're on
// colIndex is where the column for the current cell should start
function createTable(table, cells, rowIndex, colIndex)
{
 // get the current row, add if its not there yet
 var tr = table.rows[rowIndex] || table.insertRow();
 
 // how many columns in this group
 var colCount = cells.length;
 
 // iterate through all the columns
 for(var i = 0, numCells = cells.length; i < numCells; ++i)
 {
  // get the current cell
  var currentCell = cells[i];
  
  // we need to see where the last column for the current row is
  // we have to iterate through all the existing cells and add their colSpan value
  var columnEndIndex = 0;
  for(var j = 0, numCellsInThisRow = tr.cells.length; j < numCellsInThisRow; ++j)
  {
   columnEndIndex += tr.cells[j].colSpan || 1;
  }
  
  // now we know the last column in the row
  // we need to see where the column for this cell starts and add fillers in between
  var fillerLength = colIndex - columnEndIndex;
  while(fillerLength-- > 0)
  {
   tr.insertCell();
  }
  
  // now add the cell we want
  var td = tr.insertCell();
  
  // set the value
  td.innerText = currentCell.label;
  
  // if there are children
  if(currentCell.children)
  {
   // before we go to the children row
   // we need to see what the actual column for the current cell is because all the children cells will start here
   // we have to iterate through all the existing cells and add their colSpan value
   var columnEndIndex = 0;
   
   // we don't need the current cell since thats where we want to start the cells in the next row
   for(var j = 0, numCellsInThisRow = tr.cells.length - 1; j < numCellsInThisRow; ++j)
   {
    columnEndIndex += tr.cells[j].colSpan || 1;
   }
   
   // go to the next row and start making the cells
   var childSpanCount = createTable(table, currentCell.children, rowIndex + 1, columnEndIndex);
   
   // we want to add to this recursions total column count
   colCount += childSpanCount - 1;
   
   // set the colspan for this cell
   td.colSpan = childSpanCount;
  }
 }
 
 // return the total column count we have so far so it can be used in the previous recursion
 return colCount;
}

function doIt()
{
 var numCols = createTable(document.getElementById("output"), columns, 0, 0);
 alert("total number of columns: " + numCols);
}
<html>

<body>
  <a href="#" onclick="doIt(); return false">do it</a>
  <br /><br />
  <table id="output" border="1"></table>
</body>

</html>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接