从集合列表创建树<T>

Creating a tree from a collection List<T>

为了创建树,我使用了以下代码。

var db = _context.GetContext();
        var accounts = await db
            .Set<TradingAccount>()
            .ToListAsync(cancellationToken: token);

        accounts.ForEach(
            account => account.Children = accounts.Where(
                child => child.ParentTradingAccountId == account.Id).ToList()
        );

        
        return accounts;

它运行良好(虽然速度不快),但它没有创建完全正确的树。同一个元素可以既是根元素又是从属元素。如何从选择中排除已包含在树中的元素?

问题是上面的代码将依赖节点添加为子节点,但没有将它们从 top-level 列表中删除。通常递归可以用来创建树结构,像这样:

private IEnumerable<TracingAccount> GetAccounts(IEnumerable<TradingAccount> allAccounts, int parentTrackingAccountId)
{
  var accounts = allAccounts
    .Where(x => x.ParentTrackingAccountId == parentTrackingAccountId)
    .ToList();
  foreach (var acc in accounts)
  {
    // Get children of current node
    acc.Children = GetAccounts(allAccounts, acc.Id);
  }
  return accounts;
}

以上函数检索指定父 ID 的所有帐户并再次调用自身(这就是它被称为递归函数的原因)以检索子项。

您可以在您的代码中使用该函数,如下所示(我假设根级别帐户的父 ID 为 0):

var db = _context.GetContext();
var allAccounts = await db
  .Set<TradingAccount>()
  .ToListAsync(cancellationToken: token);
var accounts = GetAccounts(allAccounts, 0);
return accounts;

对 GetAccounts 的调用获取所有根级别帐户,并且 - 因为该函数会为每个帐户再次调用自身 - 通过它还检索根级别帐户的子树。

我写了一个算法来从平面列表构建树。 (比反复过滤同一个列表更快的方法) 由于项目来自数据库,因此 parentId 应该存在,并且不应出现循环引用。 这个例子无法处理这些。但它可能会给你一个jump-start如何制作更快的算法。

简而言之,循环直到添加所有项目。使用 while-loop 而不是 foreach。 foreach 的问题是您不允许在迭代时对集合进行修改。这可以通过创建副本来解决,但在许多复制操作中会 end-up。

当是child(所以parentId被填),我用查找字典查他的parent 已经添加。如果没有,我将跳过它并检查下一项。 (这使得 parent 可能低于 de 列表中的 child)。 当它被添加到 parent 时,也会将 child 添加到查找中,因此他们的 children 能够将它们添加为 child.

当它是一个parent时,我将它添加到rootNodes中,并将它添加到查找中。

支持多个根节点。

public static class TreeBuilder
{
    public static IEnumerable<Node> BuildTree(IEnumerable<Item> items)
    {
        var nodeLookup = new Dictionary<int, Node>();
        var rootNodes = new List<Node>();
        var itemCopy = items.ToList(); // we don't want to modify the original collection, make one working copy.
        int index = 0;

        while (true)
        {
            // when the item copy is empty, we're done.
            if (itemCopy.Count == 0)
                return rootNodes.ToArray();

            // do go out of bounds.
            index = index % itemCopy.Count;

            // get the current item on that index.
            var current = itemCopy[index];
            
            // does it have a parent?
            if(current.ParentId.HasValue)
            {
                // yes, so, it's a child
                // look if the parent is already found in the lookup.
                if (nodeLookup.TryGetValue(current.ParentId.Value, out var parentNode))
                {
                    // create a new node
                    var node = new Node { Id = current.Id };
                    // add it to the lookup
                    nodeLookup.Add(current.Id, node);
                    // add it as child node to the parent.
                    parentNode.ChildNodes.Add(node);
                    // remove it from the itemCopy (so don't check it again)
                    itemCopy.RemoveAt(index);

                    // The index doesn't need to be increase, because the current items is removed.
                }
                else
                    // next item, the parent is not in the tree yet.
                    index++;

            }
            else
            {
                // root node
                var node = new Node { Id = current.Id };
                nodeLookup.Add(current.Id, node);
                rootNodes.Add(node);
                itemCopy.RemoveAt(index);
            }
        }
    }
}

我的测试设置:

private void button4_Click(object sender, EventArgs e)
{
    /* 
        * 1
        * |
        * +- 2
        *    |
        *    +- 3
        *    |
        *    +- 5
        *       |
        *       +- 4
        */


    var items = new[]
    {
        new Item{ Id = 1, ParentId = null},
        new Item{ Id = 2, ParentId = 1},
        new Item{ Id = 3, ParentId = 2},
        new Item{ Id = 4, ParentId = 5},
        new Item{ Id = 5, ParentId = 2},
    };



    var tree = TreeBuilder.BuildTree(items);

    DisplayTree(tree);
}

private void DisplayTree(IEnumerable<Node> nodes, string indent = "")
{
    foreach (var node in nodes)
    {
        Trace.WriteLine($"{indent}{node.Id}");
        DisplayTree(node.ChildNodes, indent + "  ");
    }
}

我用的类是:

public class Node
{
    public int Id { get; set; }
    public List<Node> ChildNodes { get; } = new List<Node>();

}

public class Item
{
    public int Id { get; set; }
    public int? ParentId { get; set; }
}

这导致:

1
  2
    3
    5
      4