web-dev-qa-db-fra.com

Comment aplatir l'arbre via LINQ?

J'ai donc un arbre simple:

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

J'ai un IEnumerable<MyNode>. Je veux obtenir une liste de tous les MyNode (y compris les objets du nœud interne (Elements)) en une seule liste plate Wheregroup == 1. Comment faire une telle chose via LINQ?

82
myWallJSON

Vous pouvez aplatir un arbre comme ceci:

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

Vous pouvez ensuite filtrer par group en utilisant Where(...).

Pour gagner des "points pour le style", convertissez Flatten en une fonction d'extension dans une classe statique.

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

Pour gagner des points pour un "style encore meilleur", convertissez Flatten en une méthode d'extension générique qui prend un arbre et une fonction qui produit des descendants:

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

Appelez cette fonction comme ceci:

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

Si vous préférez l'aplatissement en pré-commande plutôt qu'en post-commande, basculez sur les côtés de la Concat(...).

129
dasblinkenlight

Le problème avec la réponse acceptée est qu'elle est inefficace si l'arbre est profond. Si l'arbre a une profondeur de très alors il fait exploser la pile. Vous pouvez résoudre le problème en utilisant une pile explicite:

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

En supposant n nœuds dans un arbre de hauteur h et un facteur de branchement considérablement inférieur à n, cette méthode est O(1) dans l'espace de pile, O(h) = dans l'espace du tas et O(n) dans le temps. L'autre algorithme donné est O(h) dans la pile, O(1) en tas et O(nh) en temps. Si le facteur de branchement est petit par rapport à n, alors h est compris entre O (lg n) et O (n), ce qui illustre que l'algorithme naïf peut utiliser une quantité dangereuse de pile et une grande quantité de temps si h est proche de n.

Maintenant que nous avons une traversée, votre requête est simple:

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

Juste pour être complet, voici la combinaison des réponses de dasblinkenlight et Eric Lippert. Testé à l'unité et tout. :-)

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

Mise à jour:

Pour les personnes intéressées par le niveau de nidification (profondeur). L'une des bonnes choses à propos de l'implémentation explicite de la pile d'énumérateurs est qu'à tout moment (et en particulier lors de la production de l'élément) le stack.Count représente la profondeur de traitement en cours. Donc, en tenant compte de cela et en utilisant les tuples de valeur C # 7.0, nous pouvons simplement changer la déclaration de méthode comme suit:

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

et l'instruction yield:

yield return (item, stack.Count);

Ensuite, nous pouvons implémenter la méthode d'origine en appliquant un simple Select sur ce qui précède:

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

Original:

Étonnamment, personne (même Eric) n'a montré le port itératif "naturel" d'une DFT pré-commande récursive, alors voici:

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

J'ai trouvé quelques petits problèmes avec les réponses données ici:

  • Que faire si la liste initiale des éléments est nulle?
  • Et s'il y a une valeur nulle dans la liste des enfants?

Construit sur les réponses précédentes et est venu avec les éléments suivants:

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

Et les tests unitaires:

[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
Doug Clutter

Dans le cas où quelqu'un d'autre trouverait cela, mais aurait également besoin de connaître le niveau après avoir aplati l'arbre, cela étend la combinaison de Konamiman de dasblinkenlight et des solutions d'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));
        }
    }
4
Dave

Une toute autre option consiste à avoir une conception OO appropriée.

par exemple. demandez au MyNode de renvoyer tout à plat.

Comme ça:

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

Vous pouvez maintenant demander au MyNode de niveau supérieur d'obtenir tous les nœuds.

var flatten = topNode.GetAllNodes();

Si vous ne pouvez pas modifier la classe, ce n'est pas une option. Mais sinon, je pense que cela pourrait être préféré à une méthode LINQ (récursive) distincte.

Ceci utilise LINQ, donc je pense que cette réponse est applicable ici;)

1
Julian

Ci-dessous, le code d'Ivan Stoev avec la fonctionnalité supplémentaire de dire l'index de chaque objet dans le chemin. Par exemple. rechercher "Item_120":

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

retournerait l'élément et un tableau int [1,2,0]. De toute évidence, le niveau d'imbrication est également disponible, en tant que longueur du tableau.

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();
    }
}
0
lisz
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;
}
0
Tom Brothers

Combiner la réponse de Dave et Ivan Stoev au cas où vous auriez besoin du niveau d'imbrication et la liste aplatie "dans l'ordre" et non inversée comme dans la réponse donnée par 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();
            }
        }
    }
0
Corcus

En s'appuyant sur la réponse de Konamiman et le commentaire que la commande est inattendue, voici une version avec un paramètre de tri explicite:

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

Et un exemple d'utilisation:

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