Como nivelar a árvore via LINQ?


95

Portanto, tenho uma árvore simples:

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

Eu tenho um IEnumerable<MyNode>. Quero obter uma lista de todos MyNode(incluindo objetos de nó interno ( Elements)) como uma lista plana Where group == 1. Como fazer isso via LINQ?


1
Em que ordem você deseja que a lista simplificada esteja?
Philip

1
Quando os nós param de ter nós filhos? Presumo que seja quando Elementsé nulo ou vazio?
Adam Houldsworth


A maneira mais fácil / clara de resolver isso é usar uma consulta LINQ recursiva. Esta pergunta: stackoverflow.com/questions/732281/expressing-recursion-in-linq tem muita discussão sobre isso, e esta resposta em particular é detalhada sobre como você a implementaria.
Alvaro Rodriguez

Respostas:


137

Você pode achatar uma árvore assim:

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

Você pode filtrar groupusando Where(...).

Para ganhar alguns "pontos por estilo", converta Flattenpara uma função de extensão em uma classe estática.

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

Para ganhar mais pontos por um "estilo ainda melhor", converta Flattenpara um método de extensão genérico que pega uma árvore e uma função que produz descendentes de um nó:

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

Chame essa função assim:

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

Se você preferir achatar na pré-encomenda em vez de na pós-encomenda, alterne as laterais do Concat(...).


@AdamHouldsworth Obrigado pela edição! O elemento na chamada para Concatdeveria ser new[] {e}, não new[] {c}(ele nem seria compilado com clá).
dasblinkenlight

Eu discordo: compilado, testado e trabalhando com c. Usar enão compila. Você também pode adicionar if (e == null) return Enumerable.Empty<T>();para lidar com listas de filhos nulos.
Adam Houldsworth

1
mais como `public static IEnumerable <T> Flatten <T> (esta fonte de IEnumerable <T>, Func <T, IEnumerable <T>> f) {if (source == null) return Enumerable.Empty <T> (); return source.SelectMany (c => f (c) .Flatten (f)). Concat (source); } `
myWallJSON

10
Observe que esta solução é O (nh) onde n é o número de itens na árvore eh é a profundidade média da árvore. Visto que h pode estar entre O (1) e O (n), isso está entre um algoritmo O (n) e um O (n quadrado). Existem algoritmos melhores.
Eric Lippert

1
Percebi que a função não adicionará elementos à lista achatada se a lista for de IEnumerable <baseType>. Você pode resolver isso chamando a função como esta: var res = tree.Flatten (node ​​=> node.Elements.OfType <DerivedType>)
Frank Horemans

125

O problema com a resposta aceita é que ela é ineficiente se a árvore for profunda. Se a árvore for muito profunda, ela estraga a pilha. Você pode resolver o problema usando uma pilha explícita:

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

Assumindo n nós em uma árvore de altura he um fator de ramificação consideravelmente menor que n, este método é O (1) no espaço da pilha, O (h) no espaço da pilha e O (n) no tempo. O outro algoritmo fornecido é O (h) na pilha, O (1) na pilha e O (nh) no tempo. Se o fator de ramificação for pequeno em comparação com n, então h está entre O (lg n) e O (n), o que ilustra que o algoritmo ingênuo pode usar uma quantidade perigosa de pilha e uma grande quantidade de tempo se h for próximo de n.

Agora que temos uma travessia, sua consulta é direta:

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

3
@johnnycardy: Se você fosse discutir um ponto, talvez o código não esteja obviamente correto. O que poderia torná-lo mais claramente correto?
Eric Lippert

3
@ebramtharwat: Correto. Você pode convocar Traversetodos os elementos. Ou você pode modificar Traversepara obter uma sequência e fazer com que ela insira todos os elementos da sequência stack. Lembre-se, stacké "elementos que ainda não atravessei". Ou você pode fazer uma raiz "dummy" onde sua sequência são seus filhos e, em seguida, atravessar a raiz fictícia.
Eric Lippert

2
Se você fizer foreach (var child in current.Elements.Reverse())isso, obterá um nivelamento mais esperado. Em particular, os filhos aparecerão na ordem em que aparecem, em vez do último filho primeiro. Isso não deveria importar na maioria dos casos, mas no meu caso eu precisava que o nivelamento estivesse em uma ordem previsível e esperada.
Micah Zoltu

2
@MicahZoltu, você pode evitar o .Reversetrocando o Stack<T>por umQueue<T>
Rubens Farias

2
@MicahZoltu Você está correto sobre a ordem, mas o problema Reverseé que ela cria iteradores adicionais, que é o que essa abordagem pretende evitar. @RubensFarias Substituindo Queuepor Stackresultados em travessia em largura.
Jack A.

25

Apenas para completar, aqui está a combinação das respostas de dasblinkenlight e Eric Lippert. Unidade testada e tudo mais. :-)

 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
Para evitar NullReferenceException var children = getChildren (current); if (children! = null) {foreach (var child in children) stack.Push (child); }
serg

2
Gostaria de observar que, embora isso torne a lista plana, ela a retorna na ordem inversa. O último elemento torna-se o primeiro etc.
Corcus

21

Atualizar:

Para pessoas interessadas no nível de aninhamento (profundidade). Uma das coisas boas sobre a implementação explícita da pilha do enumerador é que a qualquer momento (e em particular ao produzir o elemento), o stack.Countrepresenta a profundidade de processamento atual. Portanto, levando isso em consideração e utilizando as tuplas de valor do C # 7.0, podemos simplesmente alterar a declaração do método da seguinte maneira:

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

e yielddeclaração:

yield return (item, stack.Count);

Então podemos implementar o método original aplicando simples Selectno acima:

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

Original:

Surpreendentemente, ninguém (nem mesmo Eric) mostrou a porta iterativa "natural" de uma DFT de pré-encomenda recursiva, então aqui está:

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

Presumo que você mude a ecada vez que ligar elementSelectorpara manter a encomenda - se a ordem não importasse, você poderia alterar a função para processar tudo uma evez iniciado?
NetMage

@NetMage, eu queria fazer uma pré-encomenda especificamente. Com pequenas mudanças, ele pode lidar com pedidos de postagem. Mas o ponto principal é que este é o Depth First Traversal . Eu usaria para Breath First TraversalQueue<T> . De qualquer forma, a ideia aqui é manter uma pequena pilha com os enumeradores, bem parecida com o que está acontecendo na implementação recursiva.
Ivan Stoev

@IvanStoev Eu estava pensando que o código seria simplificado. Acho que usar o Stackresultaria em um primeiro cruzamento de largura em zigue-zague.
NetMage

7

Encontrei alguns pequenos problemas com as respostas fornecidas aqui:

  • E se a lista inicial de itens for nula?
  • E se houver um valor nulo na lista de filhos?

Com base nas respostas anteriores e chegou ao seguinte:

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

E os testes de unidade:

[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

Caso alguém encontre isso, mas também precise saber o nível depois de achatar a árvore, isso se expande na combinação de dasblinkenlight da Konamiman e nas soluções de 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));
        }
    }

2

Uma opção realmente diferente é ter um design OO adequado.

por exemplo, peça ao MyNodepara retornar todo plano.

Como isso:

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

Agora você pode pedir ao nível superior MyNode para obter todos os nós.

var flatten = topNode.GetAllNodes();

Se você não pode editar a classe, então esta não é uma opção. Mas, caso contrário, acho que isso poderia ser preferido de um método LINQ separado (recursivo).

Isso é usando LINQ, então eu acho que esta resposta é aplicável aqui;)


Talvez Enumerabl.Empty seja melhor do que a nova List?
Frank

1
De fato! Atualizada!
Julian

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
usar um foreach em sua extensão significa que não é mais 'execução atrasada' (a menos, é claro, que você use retorno de rendimento).
Tri Q Tran

0

Combinando a resposta de Dave e Ivan Stoev no caso de você precisar do nível de aninhamento e da lista nivelada "na ordem" e não invertida como na resposta dada pela 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();
            }
        }
    }

Também seria bom ser capaz de especificar primeiro a profundidade ou a largura primeiro ...
Hugh

0

Com base na resposta da Konamiman e no comentário de que a ordem é inesperada, aqui está uma versão com um parâmetro de classificação explícito:

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

E um exemplo de uso:

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

0

Abaixo está o código de Ivan Stoev com o recurso adicional de informar o índice de cada objeto no caminho. Por exemplo, pesquise "Item_120":

Item_0--Item_00
        Item_01

Item_1--Item_10
        Item_11
        Item_12--Item_120

retornaria o item e um array int [1,2,0]. Obviamente, o nível de aninhamento também está disponível, conforme o comprimento da matriz.

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

Olá, @lisz, onde você cola este código? Recebo erros como "O modificador 'público' não é válido para este item", "O modificador 'estático' não é válido para este item"
Kynao

0

Aqui está uma implementação pronta para usar usando Queue e retornando a árvore Flatten para mim primeiro e depois para meus filhos.

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

De vez em quando, tento resolver esse problema e conceber minha própria solução que ofereça suporte a estruturas arbitrariamente profundas (sem recursão), execute a travessia de largura primeiro e não abuse de muitas consultas LINQ ou execute preventivamente a recursão nos filhos. Depois de vasculhar o código - fonte .NET e tentar muitas soluções, finalmente descobri essa solução. Acabou ficando muito próximo da resposta de Ian Stoev (cuja resposta eu só vi agora), porém o meu não utiliza loops infinitos ou tem fluxo de código incomum.

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

Um exemplo prático pode ser encontrado aqui .

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.