【问题标题】:Sort hierarchy with path and depth fields using Linq使用 Linq 对具有路径和深度字段的层次结构进行排序
【发布时间】:2016-03-10 15:57:38
【问题描述】:

我已经实现了如下层次接口

interface ITree<T>
{
    // Incremential unique key
    int Id { get; set; }
    string Name { get; set; }
    // Hierarchy classical pattern
    T Parent { get; set; }
    ICollection<T> Children { get; set; }
    // Computed values
    int Depth { get; }
    // Hierarchy path with dotted notation (e.g.: 12.45.554.23,
    // where each path segment is an Id)
    string Path { get; set; }
}

class TreeItem : ITree<TreeItem>
{
    public int Id { get; set; }
    public string Name { get; set; }
    public TreeItem Parent { get; set; }
    public ICollection<TreeItem> Children { get; set; }
    public string Path { get; set; }    
    public int Depth { get { return Path.Split('.').Length - 1; } }
}

这些项目是通过实体框架存储和检索的,因此我们可以假设所有的关系字段都不为空且一致:

  • PathDepth 始终是最新的
  • 链接作品(例如:item.Parent?.Parent?.Parent
  • 遍历Children 字段也是递归的。
  • 使用PathDepth 是首选方法,因为它不需要计算关系字段。

考虑我有以下层次结构:

- A (Depth = 0)
-- B (Depth = 1)
-- C (Depth = 1)
- D  (Depth = 0)
-- E (Depth = 1)

我所有的元素都在一个无序的平面数组中,比如说 [D,C,B,E,A]。 我想使用 Linq 表达式按以下方式对它们进行排序:

  • 第一级 0,根据其名称字段
  • 前一个的所有 1 级子级,按名称排序
  • 第二级 0(仍根据其名称字段)
  • 之前的所有 1 级孩子...

该示例针对 2 级深度给出,但我希望表达式遍历层次结构,无论其深度如何。

请注意,我的数据结构的级别和路径字段可用于实现此目的,因为每当添加、移动或删除项目时,树的所有路径都会重新构建,并且深度字段是通过简单的拆分计算的('.') 在路径上。

测试样本:

var A = new TreeItem { Id = 1, Name = "A", Path = "1" };
var B = new TreeItem { Id = 2, Name = "B", Path = "1.2", Parent = A };
var C = new TreeItem { Id = 3, Name = "C", Path = "1.3", Parent = A };
var D = new TreeItem { Id = 4, Name = "D", Path = "4" };
var E = new TreeItem { Id = 5, Name = "E", Path = "4.5", Parent = D };

// populate children for the example.
// My actual code is automatic thanks to EF Inverse Relationship.
A.Children = new List<TreeItem> { B, C };
D.Children = new List<TreeItem> { E };

var listToSortHierarchically = new List<TreeItem> { D, C, B, E, A };
// I want the result of the hierarchical sort to be A B C D E

【问题讨论】:

  • 什么是T?您能否提供一个示例实现。而且不应该至少限制为where T : ITree&lt;T&gt;
  • 我添加了一个示例实现。基本上,T 是一个 POCO 对象,Tree 接口确保它具有 Id、Name、Parent、Children、Depth 和 Path 字段的外观。
  • 谢谢。它不是编译,但无论如何。约束怎么样?我的意思是,如果我有ITree&lt;TreeItem&gt; node,我可以使用node.Parent.Parent 等吗?没有约束,我需要强制转换吗?
  • 好的,快速编辑以获得可编译的模型并回答您的问题,但我将提供一个代码示例来设置层次结构。

标签: c# linq sorting tree


【解决方案1】:

好的,首先你应该真正添加以下约束

interface ITree<T>
    where T : class, ITree<T>
{
    // ...
}

所以我们可以使用 ParentChildren 属性安全地导航层次结构,而无需强制转换。

其次,我不会重新发明轮子,而是重用通用树,以便从我对How to flatten tree via LINQ?(以及其他几个)的回答中遍历辅助方法:

public static partial class TreeUtils
{
    public static IEnumerable<T> Expand<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
    {
        var stack = new Stack<IEnumerator<T>>();
        var e = source.GetEnumerator();
        try
        {
            while (true)
            {
                while (e.MoveNext())
                {
                    var item = e.Current;
                    yield return item;
                    var elements = elementSelector(item);
                    if (elements == null) continue;
                    stack.Push(e);
                    e = elements.GetEnumerator();
                }
                if (stack.Count == 0) break;
                e.Dispose();
                e = stack.Pop();
            }
        }
        finally
        {
            e.Dispose();
            while (stack.Count != 0) stack.Pop().Dispose();
        }
    }
}

口袋里有那个助手,有问题的方法很简单:

partial class TreeUtils
{
    public static IEnumerable<T> Ordered<T>(this IEnumerable<T> source, Func<IEnumerable<T>, IEnumerable<T>> order = null)
        where T : class, ITree<T>
    {
        if (order == null) order = items => items.OrderBy(item => item.Name);
        return order(source.Where(item => item.Parent == null))
            .Expand(item => item.Children != null && item.Children.Any() ? order(item.Children) : null);
    }
}

示例用法:

List<TreeItem> flatList = ...;
var orderedList = flatList.Ordered().ToList();

更新:这里只使用 PathId 属性是一样的:

public static partial class TreeUtils
{
    public static IEnumerable<T> Ordered<T>(this IEnumerable<T> source, Func<IEnumerable<T>, IEnumerable<T>> order = null)
        where T : class, ITree<T>
    {
        if (order == null) order = items => items != null && items.Any() ?  items.OrderBy(item => item.Name) : null;
        var chldrenByParentId = source
            .Select(item => new { item, path = item.Path.Split('.') })
            .ToLookup(e => e.path.Length >= 2 ? int.Parse(e.path[e.path.Length - 2]) : (int?)null, e => e.item);
        return (order(chldrenByParentId[null]) ?? Enumerable.Empty<T>())
            .Expand(item => order(chldrenByParentId[item.Id]));
    }
}

【讨论】:

  • 感谢您的准确回答...但实际上,我期待的答案仅涉及路径字段(以及来自路径的深度)。考虑到我们实际上已经填充了 Parent 和 Children 字段,您的答案在遍历和展平或重建树时当然是出色和通用的。但如果可能的话,我想避免使用这些,因为对于性能问题,我最终可能决定不将它们填充到我的平面列表中进行排序。希望我足够清楚!
  • 如你所愿。这是可行的,但是 IMO 拆分和解析路径以及线性搜索平面列表将非常低效。
  • 好的,我将研究如何在我的情况下设置您的方法。我必须承认我没有关于树遍历和查找算法的理论知识,我应该改进它!
  • 无论如何,为了您的方便,提供了不使用Parent/Children的替代方法。享受:)
猜你喜欢
  • 1970-01-01
  • 2016-05-21
  • 2022-01-08
  • 2020-12-25
  • 2019-12-27
  • 2013-01-13
  • 1970-01-01
  • 2012-08-27
  • 1970-01-01
相关资源
最近更新 更多