数组中嵌套对象的JavaScript递归函数

时间:2016-05-26 15:14:43

标签: javascript arrays algorithm recursion

我尝试实现一种算法来生成带有分层标头的表。这些可以无限制嵌套。 渲染表标记的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>  
&#13;
&#13;
&#13;

表的配置应该以这种格式作为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]
]; 

其中每个条目表示包含其列定义(tr)的表行(td),数字表示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;
}

它按预期工作,但我一直试图生成&#34;结构&#34; array ...我当前(令人尴尬的)代码尝试是这样的:

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

你能帮助我吗?

3 个答案:

答案 0 :(得分:3)

棘手的部分是计算跨度,即给定节点下的叶节点数或1节点是叶本身。该值可以递归定义,如下所示:

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

其余的非常简单:

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

答案 1 :(得分: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 ] ]

(我不确切知道如何处理“不同高度”的特征......结构应该如何?)

答案 2 :(得分: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>