Como atravessar uma árvore sem usar recursão?


19

Eu tenho uma árvore de nós de memória muito grande e preciso atravessá-la. Passando os valores retornados de cada nó filho para o nó pai. Isso deve ser feito até que todos os nós tenham seus dados em bolha até o nó raiz.

O Traversal funciona assim.

private Data Execute(Node pNode)
{
    Data[] values = new Data[pNode.Children.Count];
    for(int i=0; i < pNode.Children.Count; i++)
    {
        values[i] = Execute(pNode.Children[i]);  // recursive
    }
    return pNode.Process(values);
}

public void Start(Node pRoot)
{
    Data result = Execute(pRoot);
}

Isso funciona bem, mas estou preocupado que a pilha de chamadas limite o tamanho da árvore de nós.

Como o código pode ser reescrito para que não Executesejam feitas chamadas recursivas ?


8
Você precisaria manter sua própria pilha para acompanhar os nós ou alterar a forma da árvore. Veja stackoverflow.com/q/5496464 e stackoverflow.com/q/4581576
Robert Harvey

1
Também encontrei muita ajuda nesta Pesquisa do Google , especificamente Morris Traversal .
Robert Harvey

@RobertHarvey, obrigado Rob, eu não tinha certeza de quais termos isso seria.
Reactgular

2
Você pode se surpreender com os requisitos de memória se fizer as contas. Por exemplo, uma árvore binária de teranodo perfeitamente equilibrada precisa apenas de uma pilha com 40 entradas de profundidade.
Karl Bielefeldt

@KarlBielefeldt Isso assume que a árvore está perfeitamente equilibrada. Às vezes, você precisa modelar árvores que não são equilibradas e, nesse caso, é muito fácil explodir a pilha.
Servy

Respostas:


27

Aqui está uma implementação transversal de árvore de uso geral que não usa recursão:

public static IEnumerable<T> Traverse<T>(T item, Func<T, IEnumerable<T>> childSelector)
{
    var stack = new Stack<T>();
    stack.Push(item);
    while (stack.Any())
    {
        var next = stack.Pop();
        yield return next;
        foreach (var child in childSelector(next))
            stack.Push(child);
    }
}

No seu caso, você pode chamá-lo assim:

IEnumerable<Node> allNodes = Traverse(pRoot, node => node.Children);

Use uma pesquisa em Queuevez de a Stackpara respirar primeiro, em vez da profundidade primeiro. Use a PriorityQueuepara obter a melhor primeira pesquisa.


Estou correto ao pensar que isso apenas achatará a árvore em uma coleção?
Reactgular

1
@MathewFoscarini Sim, esse é o seu propósito. Obviamente, ele não precisa necessariamente ser materializado em uma coleção real. É apenas uma sequência. Você pode iterar sobre ele para transmitir os dados, sem precisar puxar todo o conjunto de dados para a memória.
Servy

Eu não acho que isso resolve o problema.
Reactgular

4
Ele não está apenas percorrendo o gráfico executando operações independentes como uma pesquisa, ele está agregando os dados dos nós filhos. O achatamento da árvore destrói as informações de estrutura necessárias para executar a agregação.
Karl Bielefeldt

1
Para sua informação, acho que esta é a resposta correta que a maioria das pessoas pesquisando esta questão no Google. +1
Anders Arpi

4

Se você tem uma estimativa da profundidade da sua árvore de antemão, talvez seja suficiente para o seu caso adaptar o tamanho da pilha? No C # desde a versão 2.0, isso é possível sempre que você inicia um novo thread, veja aqui:

http://www.atalasoft.com/cs/blogs/rickm/archive/2008/04/22/increasing-the-size-of-your-stack-net-memory-management-part-3.aspx

Dessa forma, você pode manter seu código recursivo, sem precisar implementar algo mais complexo. Obviamente, criar uma solução não recursiva com sua própria pilha pode ser mais eficiente em termos de tempo e memória, mas tenho certeza de que o código não será tão simples quanto é por enquanto.


Acabei de fazer um teste rápido. Na minha máquina, eu poderia fazer 14000 chamadas recursivas antes de atingir o fluxo de pilha. Se a árvore estiver equilibrada, são necessárias apenas 32 chamadas para armazenar 4 bilhões de nós. Se cada nó é 1 byte (que não vai ser) Levaria 4 GB de memória RAM para armazenar uma árvore equilibrada de altura 32.
Esben Skov Pedersen

Eu deveríamos usar todas as 14000 chamadas na pilha. A árvore levaria até 2,6x10 ^ 4214 bytes se cada nó é um byte (que não vai ser)
Esben Skov Pedersen

-3

Você não pode atravessar uma estrutura de dados na forma de uma árvore sem usar recursão - se você não usar os quadros de pilha e as chamadas de função fornecidas pelo seu idioma, basicamente precisará programar suas próprias chamadas de pilha e função, e é improvável que você consiga fazê-lo na linguagem de maneira mais eficiente do que os escritores do compilador fizeram na máquina em que seu programa seria executado.

Portanto, evitar recursões por medo de atingir os limites de recursos geralmente é equivocado. Para garantir, a otimização prematura de recursos é sempre equivocada, mas, neste caso, é provável que, mesmo que você avalie e confirme que o uso de memória é o gargalo, provavelmente não será possível aprimorá-lo sem diminuir o nível do escritor do compilador.


2
Isto é simplesmente falso. É certamente possível atravessar uma árvore sem usar recursão. Nem é difícil . Você também pode fazê-lo de forma mais eficiente e trivial, pois só pode incluir na pilha explícita o número de informações necessárias para a sua travessia específica, enquanto o uso da recursão acaba armazenando mais informações do que realmente é necessário em muitos casos.
Servy

2
Esta controvérsia surge de vez em quando aqui. Alguns pôsteres consideram que rolar sua própria pilha não é recursão, enquanto outros apontam que eles estão apenas fazendo a mesma coisa explicitamente que o tempo de execução faria de outra maneira implicitamente. Não faz sentido discutir definições como essa.
precisa saber é o seguinte

Como você define a recursão então? Eu o definiria como uma função que se chama dentro de sua própria definição. Certamente você pode atravessar uma árvore sem fazer isso, como demonstrei em minha resposta.
Servy

2
Eu sou mau por gostar do ato de clicar no voto negativo em alguém com uma pontuação tão alta? É um prazer tão raro neste site.
Reactgular

2
Vamos lá, Mat, isso é coisa de criança. Você pode discordar, como se você tem medo de bombardear uma árvore muito profunda, é uma preocupação razoável. Você pode apenas dizer isso.
precisa saber é o seguinte
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.