Як сплющити дерево через LINQ?


95

Отже, у мене є просте дерево:

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

У мене є IEnumerable<MyNode>. Я хочу отримати список усіх MyNode(включаючи об'єкти внутрішнього вузла ( Elements)) як один плоский список Where group == 1. Як це зробити через LINQ?


1
У якому порядку ви хочете, щоб був сплощений список?
Філіп

1
Коли вузли перестають мати дочірні вузли? Я припускаю, що це коли Elementsнуль або порожній?
Адам Голдсворт,

може дублюватися зі stackoverflow.com/questions/11827569/…
Tamir

Найпростіший / найбільш зрозумілий спосіб вирішення цього питання - використання рекурсивного запиту LINQ. Це питання: stackoverflow.com/questions/732281/expressing-recursion-in-linq має багато дискусій з цього приводу, і ця конкретна відповідь докладно розглядається щодо того, як би ви його реалізували.
Альваро Родрігес,

Відповіді:


138

Ви можете сплющити дерево так:

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

Потім ви можете фільтрувати за groupдопомогою Where(...).

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

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

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

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

Викличте цю функцію так:

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

Якщо ви віддаєте перевагу згладжуванню в попередньому замовленні, а не в замовленні, перейдіть по боках Concat(...).


@AdamHouldsworth Дякуємо за редагування! Елементом у виклику Concatповинно бути new[] {e}, а не new[] {c}(він навіть не компілюється cтам).
dasblinkenlight

Я не згоден: зібрано, перевірено та працюю з ними c. Використання eне компілюється. Ви також можете додати, if (e == null) return Enumerable.Empty<T>();щоб справлятися з нульовими дочірніми списками.
Адам Голдсворт,

1
більше схоже на `public static IEnumerable <T> Flatten <T> (this IEnumerable <T> source, Func <T, IEnumerable <T>> f) {if (source == null) return Enumerable.Empty <T> (); повернути джерело. ВиберітьБагато (c => f (c) .Флаттер (f)). Concat (джерело); } `
myWallJSON

10
Зверніть увагу, що це рішення має значення O (nh), де n - кількість елементів у дереві, а h - середня глибина дерева. Оскільки h може знаходитись між O (1) та O (n), це між O (n) та O (n у квадраті) алгоритму. Є кращі алгоритми.
Ерік Ліпперт,

1
Я помітив, що функція не додаватиме елементів до сплощеного списку, якщо список має IEnumerable <baseType>. Вирішити це можна, викликавши функцію так: var res = tree.Flatten (node ​​=> node.Elements.OfType <DerivedType>)
Frank Horemans,

125

Проблема прийнятої відповіді полягає в тому, що вона неефективна, якщо дерево глибоке. Якщо дерево дуже глибоке, тоді воно продуває стек. Ви можете вирішити проблему, використовуючи явний стек:

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
@johnnycardy: Якщо ви збиралися аргументувати свою думку, то, можливо, код не є очевидно правильним. Що може зробити його більш чітко правильним?
Ерік Ліпперт,

3
@ebramtharwat: Правильно. Ви можете Traverseвикористовувати всі елементи. Або ви можете модифікувати, Traverseщоб взяти послідовність, і попросити її висунути всі елементи послідовності stack. Пам'ятайте, stackце "елементи, які я ще не пройшов". Або ви можете створити "фіктивний" корінь там, де ваша послідовність - це його діти, а потім обвести фіктивний корінь.
Ерік Ліпперт,

2
Якщо ви це зробите, foreach (var child in current.Elements.Reverse())ви отримаєте більш очікуване сплющення. Зокрема, діти з’являтимуться в тому порядку, в якому вони з’являються, а не остання дитина першою. Це не повинно мати значення в більшості випадків, але в моєму випадку мені потрібно було, щоб сплющення було в передбачуваному та очікуваному порядку.
Micah Zoltu

2
@MicahZoltu, ви могли б уникнути .Reverseшляхом заміни Stack<T>наQueue<T>
Рубенс Farias

2
@MicahZoltu Ви маєте рацію щодо замовлення, але проблема Reverseполягає в тому, що він створює додаткові ітератори, саме цього цей підхід і слід уникати. @RubensFarias Підстановка Queueдля Stackрезультатів в ширину-перше обходу.
Джек А.

25

Тільки для повноти, ось комбінація відповідей від dasblinkenlight та Еріка Ліпперта. Блок перевірено і все. :-)

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

3
Щоб уникнути NullReferenceException var children = getChildren (поточний); if (kids! = null) {foreach (var child у дітей) stack.Push (child); }
serg

2
Я хотів би зауважити, що хоча це і вирівнює список, він повертає його в зворотному порядку. Останній елемент стає першим тощо.
Коркус,

21

Оновлення:

Для людей, яких цікавить рівень вкладеності (глибина). Однією з хороших речей явної реалізації стеку перечислювача є те, що в будь-який момент (і, особливо, при виведенні елемента), stack.Countрепрезентує поточну глибину обробки. Отже, беручи це до уваги та використовуючи кортежі значень C # 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);

Оригінал:

Дивно, але ніхто (навіть Ерік) не показав "природний" ітераційний порт рекурсивного попереднього замовлення DFT, тож ось:

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

Я припускаю, що ви переключаєтесь eкожного разу, коли ви телефонуєте, elementSelectorщоб підтримувати попереднє замовлення - якщо замовлення не мало значення, чи могли б ви змінити функцію на обробку кожного з кожного eзапуску?
NetMage

@NetMage Я хотів спеціально замовити замовлення. З невеликими змінами він може обробляти після замовлення. Але головне, це обхід глибини першої . Для обходу першого вдиху я б використав Queue<T>. У будь-якому випадку, ідея тут полягає в тому, щоб зберегти невеликий стек з перелічувачами, дуже схожий на те, що відбувається в рекурсивній реалізації.
Іван Стоєв

@IvanStoev Я думав, що код буде спрощений. Я гадаю, що використання Stackрезультату призведе до зигзагоподібного першого обходу.
NetMage

7

Я знайшов кілька невеликих питань із наведеними тут відповідями:

  • Що робити, якщо початковий список елементів дорівнює нулю?
  • Що робити, якщо у списку дітей є нульове значення?

На основі попередніх відповідей і придумав наступне:

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

4

Якщо хтось ще знайде це, але йому також потрібно знати рівень після того, як вони сплюснули дерево, це розширюється на поєднанні Konamiman dasblinkenlight та рішеннях Еріка Ліпперта:

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

2

Насправді іншим варіантом є належний дизайн ОО.

наприклад, попросіть MyNodeповернути всі сплющення.

Подобається це:

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

    public IEnumerable<MyNode> GetAllNodes()
    {
        if (Elements == null)
        {
            return Enumerable.Empty<MyNode>(); 
        }

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

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

var flatten = topNode.GetAllNodes();

Якщо ви не можете редагувати клас, то це не варіант. Але в іншому випадку, я думаю, це можна віддати перевагу окремому (рекурсивному) методу LINQ.

Це використовує LINQ, тому я думаю, що ця відповідь тут застосовна;)


Можливо, Enumerabl.Порожній краще, ніж новий список?
Френк

1
Справді! Оновлено!
Джуліан

0
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;
}

1
використання foreach у вашому розширенні означає, що це більше не є «відкладеним виконанням» (якщо, звичайно, ви не використовуєте return return).
Tri Q Tran

0

Поєднуючи відповідь Дейва та Івана Стоєва на випадок, якщо вам потрібен рівень вкладеності та список згладжений "по порядку", а не зворотний, як у відповіді Конамімана.

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

Також було б непогано мати можливість спочатку вказати глибину або широту ...
Х'ю,

0

Спираючись на відповідь Конамімана та коментар, що впорядкування є несподіваним, ось версія з явним параметром сортування:

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

0

Нижче наведено код Івана Стоєва з додатковою функцією передачі індексу кожного об’єкта на шляху. Наприклад, пошук за запитом "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();
    }
}

Привіт, @lisz, куди ти вставляєш цей код? Я отримую такі помилки, як "Модифікатор 'public' недійсний для цього елемента", "Модифікатор 'static' недійсний для цього елемента"
Kynao

0

Тут є деякі готові до використання реалізації за допомогою Queue і повернення дерева Flatten спочатку мені, а потім моїм дітям.

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

        var queue = new Queue<T>();

        foreach (var item in items) {
            if (item == null)
                continue;

            queue.Enqueue(item);

            while (queue.Count > 0) {
                var current = queue.Dequeue();
                yield return current;

                if (current == null)
                    continue;

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

                foreach (var child in children)
                    queue.Enqueue(child);
            }
        }

    }

0

Час від часу я намагаюся подряпати цю проблему і розробити своє власне рішення, яке підтримує довільно глибокі структури (без рекурсії), виконує обхід в першу чергу і не зловживає занадто багато запитів LINQ або попередньо виконує рекурсію для дітей. Покопавшись у джерелі .NET та спробувавши багато рішень, я нарешті придумав це рішення. Врешті-решт, це було дуже близько до відповіді Яна Стоєва (відповідь якого я бачив лише зараз), однак моя не використовує нескінченні цикли або незвичний потік коду.

public static IEnumerable<T> Traverse<T>(
    this IEnumerable<T> source,
    Func<T, IEnumerable<T>> fnRecurse)
{
    if (source != null)
    {
        Stack<IEnumerator<T>> enumerators = new Stack<IEnumerator<T>>();
        try
        {
            enumerators.Push(source.GetEnumerator());
            while (enumerators.Count > 0)
            {
                var top = enumerators.Peek();
                while (top.MoveNext())
                {
                    yield return top.Current;

                    var children = fnRecurse(top.Current);
                    if (children != null)
                    {
                        top = children.GetEnumerator();
                        enumerators.Push(top);
                    }
                }

                enumerators.Pop().Dispose();
            }
        }
        finally
        {
            while (enumerators.Count > 0)
                enumerators.Pop().Dispose();
        }
    }
}

Робочий приклад можна знайти тут .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.