Как сгладить дерево через LINQ?

Итак, у меня есть простое дерево:

class MyNode
{
 public MyNode Parent;
 public IEnumerable<MyNode> Elements;
 int group = 1;
}

У меня есть IEnumerable<MyNode>. Я хочу получить список всех MyNode (включая внутренние объекты node (Elements)) как один плоский список Where group == 1. Как это сделать через LINQ?

Ответ 1

Вы можете сгладить дерево следующим образом:

IEnumerable<MyNode> Flatten(IEnumerable<MyNode> e) {
    return e.SelectMany(c => Flatten(c.Elements)).Concat(new[] {e});
}

Затем вы можете фильтровать с помощью group с помощью Where(...).

Чтобы заработать "очки за стиль", преобразуйте Flatten в функцию расширения в статическом классе.

public static IEnumerable<MyNode> Flatten(this IEnumerable<MyNode> e) {
    return e.SelectMany(c => c.Elements.Flatten()).Concat(e);
}

Чтобы заработать несколько очков за "еще лучший стиль", преобразуйте Flatten в общий метод расширения, который берет дерево и функцию, которая производит потомки:

public static IEnumerable<T> Flatten<T>(
    this IEnumerable<T> e,
    Func<T,IEnumerable<T>> f) 
{
    return e.SelectMany(c => f(c).Flatten(f)).Concat(e);
}

Вызвать эту функцию следующим образом:

IEnumerable<MyNode> tree = ....
var res = tree.Flatten(node => node.Elements);

Если вы предпочитаете сплющивание в предварительном порядке, а не в пост-порядке, переключитесь по сторонам Concat(...).

Ответ 2

Проблема с принятым ответом заключается в том, что он неэффективен, если дерево глубокое. Если дерево очень глубокое, то оно ударяет стек. Вы можете решить проблему, используя явный стек:

public static IEnumerable<MyNode> Traverse(this MyNode root)
{
    var stack = new Stack<MyNode>();
    stack.Push(root);
    while(stack.Count > 0)
    {
        var current = stack.Pop();
        yield return current;
        foreach(var child in current.Elements)
            stack.Push(child);
    }
}

Предполагая, что n узлов в дереве высоты h и фактор ветвления значительно меньше n, этот метод является O (1) в пространстве стека, O (h) в кучном пространстве и O (n) во времени. Другим заданным алгоритмом является O (h) в стеке, O (1) в куче и O (nh) во времени. Если коэффициент ветвления мал по сравнению с n, то h находится между O (lg n) и O (n), что иллюстрирует, что наивный алгоритм может использовать опасное количество стека и большое количество времени, если h близко к n.

Теперь, когда у нас есть обход, ваш запрос прост:

root.Traverse().Where(item=>item.group == 1);

Ответ 3

Просто для полноты, вот комбинация ответов от dasblinkenlight и Eric Lippert. Единица протестирована и все.: -)

 public static IEnumerable<T> Flatten<T>(
        this IEnumerable<T> items,
        Func<T, IEnumerable<T>> getChildren)
 {
     var stack = new Stack<T>();
     foreach(var item in items)
         stack.Push(item);

     while(stack.Count > 0)
     {
         var current = stack.Pop();
         yield return current;

         var children = getChildren(current);
         if (children == null) continue;

         foreach (var child in children) 
            stack.Push(child);
     }
 }

Ответ 4

Обновить:

Для людей, интересующихся уровнем гнездования (глубина). Одна из хороших вещей, связанных с реализацией явного перечислительного стека, заключается в том, что в любой момент (и, в частности, при подаче элемента) stack.Count представляет собой stack.Count глубину обработки. Поэтому, учитывая это и используя кортежи значений С# 7.0, мы можем просто изменить объявление метода следующим образом:

public static IEnumerable<(T Item, int Level)> ExpandWithLevel<T>(
    this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)

и утверждение yield:

yield return (item, stack.Count);

Затем мы можем реализовать оригинальный метод, применяя простой Select по вышесказанному:

public static IEnumerable<T> Expand<T>(
    this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector) =>
    source.ExpandWithLevel(elementSelector).Select(e => e.Item);

Оригинал:

Удивительно, но никто (даже Эрик) не показал "естественный" итерационный порт рекурсивного предзакатного ДПФ, так вот вот он:

    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();
        }
    }

Ответ 5

В случае, если кто-либо еще это обнаружит, но также должен знать уровень после того, как они сплющили дерево, это расширяет комбинацию Konamiman из dasblinkenlight и решений Eric Lippert:

    public static IEnumerable<Tuple<T, int>> FlattenWithLevel<T>(
            this IEnumerable<T> items,
            Func<T, IEnumerable<T>> getChilds)
    {
        var stack = new Stack<Tuple<T, int>>();
        foreach (var item in items)
            stack.Push(new Tuple<T, int>(item, 1));

        while (stack.Count > 0)
        {
            var current = stack.Pop();
            yield return current;
            foreach (var child in getChilds(current.Item1))
                stack.Push(new Tuple<T, int>(child, current.Item2 + 1));
        }
    }

Ответ 6

Я нашел некоторые небольшие проблемы с ответами, приведенными здесь:

  • Что делать, если исходный список элементов равен NULL?
  • Что делать, если в списке детей есть нулевое значение?

Построенный на предыдущих ответах и придумал следующее:

public static class IEnumerableExtensions
{
    public static IEnumerable<T> Flatten<T>(
        this IEnumerable<T> items, 
        Func<T, IEnumerable<T>> getChildren)
    {
        if (items == null)
            yield break;

        var stack = new Stack<T>(items);
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            yield return current;

            if (current == null) continue;

            var children = getChildren(current);
            if (children == null) continue;

            foreach (var child in children)
                stack.Push(child);
        }
    }
}

И модульные тесты:

[TestClass]
public class IEnumerableExtensionsTests
{
    [TestMethod]
    public void NullList()
    {
        IEnumerable<Test> items = null;
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(0, flattened.Count());
    }
    [TestMethod]
    public void EmptyList()
    {
        var items = new Test[0];
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(0, flattened.Count());
    }
    [TestMethod]
    public void OneItem()
    {
        var items = new[] { new Test() };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(1, flattened.Count());
    }
    [TestMethod]
    public void OneItemWithChild()
    {
        var items = new[] { new Test { Id = 1, Children = new[] { new Test { Id = 2 } } } };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(2, flattened.Count());
        Assert.IsTrue(flattened.Any(i => i.Id == 1));
        Assert.IsTrue(flattened.Any(i => i.Id == 2));
    }
    [TestMethod]
    public void OneItemWithNullChild()
    {
        var items = new[] { new Test { Id = 1, Children = new Test[] { null } } };
        var flattened = items.Flatten(i => i.Children);
        Assert.AreEqual(2, flattened.Count());
        Assert.IsTrue(flattened.Any(i => i.Id == 1));
        Assert.IsTrue(flattened.Any(i => i == null));
    }
    class Test
    {
        public int Id { get; set; }
        public IEnumerable<Test> Children { get; set; }
    }
}

Ответ 7

Действительно другой вариант - иметь правильную ОО-конструкцию.

Например, попросите MyNode вернуть все MyNode.

Как это:

class MyNode
{
    public MyNode Parent;
    public IEnumerable<MyNode> Elements;
    int group = 1;

    public IEnumerable<MyNode> GetAllNodes()
    {
        if (Elements == null)
        {
            return new List<MyNode>(); 
        }

        return Elements.SelectMany(e => e.GetAllNodes());
    }
}

Теперь вы можете попросить MyNode верхнего уровня получить все узлы.

var flatten = topNode.GetAllNodes();

Если вы не можете редактировать класс, тогда это не вариант. Но в остальном, я думаю, это может быть предпочтительнее отдельного (рекурсивного) метода LINQ.

Это использует LINQ, поэтому я думаю, что этот ответ применим здесь;)

Ответ 8

Самый простой/самый понятный способ решения этой проблемы - использовать рекурсивный запрос LINQ. Этот вопрос: Выражение рекурсии в LINQ имеет много дискуссий по этому вопросу, и этот конкретный ответ fooobar.com/questions/26029/... подробно рассказывает о том, как вы его реализуете.

Ответ 9

void Main()
{
    var allNodes = GetTreeNodes().Flatten(x => x.Elements);

    allNodes.Dump();
}

public static class ExtensionMethods
{
    public static IEnumerable<T> Flatten<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> childrenSelector = null)
    {
        if (source == null)
        {
            return new List<T>();
        }

        var list = source;

        if (childrenSelector != null)
        {
            foreach (var item in source)
            {
                list = list.Concat(childrenSelector(item).Flatten(childrenSelector));
            }
        }

        return list;
    }
}

IEnumerable<MyNode> GetTreeNodes() {
    return new[] { 
        new MyNode { Elements = new[] { new MyNode() }},
        new MyNode { Elements = new[] { new MyNode(), new MyNode(), new MyNode() }}
    };
}

class MyNode
{
    public MyNode Parent;
    public IEnumerable<MyNode> Elements;
    int group = 1;
}

Ответ 10

Объединяя Дэйва и Ивана Стоева, ответьте, если вам нужен уровень гнездования, а список сплющен "по порядку" и не будет отменен, как в ответе, предоставленном Konamiman.

 public static class HierarchicalEnumerableUtils
    {
        private static IEnumerable<Tuple<T, int>> ToLeveled<T>(this IEnumerable<T> source, int level)
        {
            if (source == null)
            {
                return null;
            }
            else
            {
                return source.Select(item => new Tuple<T, int>(item, level));
            }
        }

        public static IEnumerable<Tuple<T, int>> FlattenWithLevel<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
        {
            var stack = new Stack<IEnumerator<Tuple<T, int>>>();
            var leveledSource = source.ToLeveled(0);
            var e = leveledSource.GetEnumerator();
            try
            {
                while (true)
                {
                    while (e.MoveNext())
                    {
                        var item = e.Current;
                        yield return item;
                        var elements = elementSelector(item.Item1).ToLeveled(item.Item2 + 1);
                        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();
            }
        }
    }

Ответ 11

На основе ответа Konamiman и комментария о том, что упорядочение неожиданно, здесь версия с явным параметром sort:

public static IEnumerable<T> TraverseAndFlatten<T, V>(this IEnumerable<T> items, Func<T, IEnumerable<T>> nested, Func<T, V> orderBy)
{
    var stack = new Stack<T>();
    foreach (var item in items.OrderBy(orderBy))
        stack.Push(item);

    while (stack.Count > 0)
    {
        var current = stack.Pop();
        yield return current;

        var children = nested(current).OrderBy(orderBy);
        if (children == null) continue;

        foreach (var child in children)
            stack.Push(child);
    }
}

И пример использования:

var flattened = doc.TraverseAndFlatten(x => x.DependentDocuments, y => y.Document.DocDated).ToList();

Ответ 12

Ниже приведен код Ивана Стоева с дополнительным признаком, указывающим индекс каждого объекта в пути. Например, найдите "Item_120":

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

будет возвращать элемент и массив int [1,2,0]. Очевидно, что уровень вложенности также доступен, как длина массива.

public static IEnumerable<(T, int[])> Expand<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> getChildren) {
    var stack = new Stack<IEnumerator<T>>();
    var e = source.GetEnumerator();
    List<int> indexes = new List<int>() { -1 };
    try {
        while (true) {
            while (e.MoveNext()) {
                var item = e.Current;
                indexes[stack.Count]++;
                yield return (item, indexes.Take(stack.Count + 1).ToArray());
                var elements = getChildren(item);
                if (elements == null) continue;
                stack.Push(e);
                e = elements.GetEnumerator();
                if (indexes.Count == stack.Count)
                    indexes.Add(-1);
                }
            if (stack.Count == 0) break;
            e.Dispose();
            indexes[stack.Count] = -1;
            e = stack.Pop();
        }
    } finally {
        e.Dispose();
        while (stack.Count != 0) stack.Pop().Dispose();
    }
}