Come posso attraversare un albero senza usare la ricorsione?


19

Ho un albero dei nodi di memoria molto grande e devo attraversare l'albero. Passando i valori restituiti di ciascun nodo figlio al nodo principale. Questo deve essere fatto fino a quando tutti i nodi hanno i loro dati in bolla sul nodo principale.

Traversal funziona così.

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

Funziona bene, ma sono preoccupato che lo stack di chiamate limiti le dimensioni dell'albero dei nodi.

Come si può riscrivere il codice in modo da non effettuare chiamate ricorsive Execute?


8
Dovresti mantenere il tuo stack per tenere traccia dei nodi o cambiare la forma dell'albero. Vedi stackoverflow.com/q/5496464 e stackoverflow.com/q/4581576
Robert Harvey

1
Ho anche trovato molto aiuto in questa Ricerca Google , in particolare Morris Traversal .
Robert Harvey,

@RobertHarvey grazie Rob, non ero sicuro di quali termini sarebbe andato sotto.
Reactgular,

2
Potresti essere sorpreso dai requisiti di memoria se hai fatto la matematica. Ad esempio, un albero binario teranode perfettamente bilanciato necessita solo di uno stack di 40 voci di profondità.
Karl Bielefeldt,

@KarlBielefeldt Questo presuppone però che l'albero sia perfettamente bilanciato. A volte è necessario modellare alberi non bilanciati e in tal caso è molto semplice far saltare in pila.
Servito il

Risposte:


27

Ecco un'implementazione di attraversamento dell'albero per scopi generici che non utilizza la ricorsione:

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

Nel tuo caso puoi quindi chiamarlo così:

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

Utilizzare prima una ricerca Queueanziché una Stacka, piuttosto che la profondità. Utilizzare a PriorityQueueper una migliore prima ricerca.


Sono corretto nel pensare che questo appiattirà l'albero in una raccolta?
Reactgular,

1
@MathewFoscarini Sì, questo è il suo scopo. Naturalmente, non deve necessariamente essere materializzato in una vera collezione. È solo una sequenza. È possibile iterare su di esso per eseguire lo streaming dei dati, senza la necessità di estrarre l'intero set di dati in memoria.
Servito il

Non penso che risolva il problema.
Reactgular,

4
Non sta solo attraversando il grafico eseguendo operazioni indipendenti come una ricerca, sta aggregando i dati dai nodi figlio. L'appiattimento dell'albero distrugge le informazioni sulla struttura di cui ha bisogno per eseguire l'aggregazione.
Karl Bielefeldt,

1
Cordiali saluti Penso che questa sia la risposta corretta che la maggior parte delle persone cercano su Google questa domanda. +1
Anders Arpi,

4

Se hai una stima della profondità del tuo albero in anticipo, forse è sufficiente per il tuo caso adattare le dimensioni dello stack? In C # dalla versione 2.0 questo è possibile ogni volta che inizi una nuova discussione, vedi qui:

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

In questo modo puoi mantenere il tuo codice ricorsivo, senza dover implementare qualcosa di più complesso. Naturalmente, la creazione di una soluzione non ricorsiva con il proprio stack potrebbe essere più efficiente in termini di tempo e memoria, ma sono abbastanza sicuro che il codice non sarà così semplice come lo è per ora.


Ho appena fatto un test veloce. Sulla mia macchina ho potuto effettuare 14000 chiamate ricorsive prima di raggiungere StackOverflow. Se l'albero è bilanciato sono necessarie solo 32 chiamate per memorizzare 4 miliardi di nodi. Se ogni nodo è 1 byte (che non sarà) sarebbero necessari 4 GB di RAM per memorizzare un albero bilanciato di altezza 32.
Esben Skov Pedersen

Avremmo dovuto usare tutte le 14000 chiamate nello stack. L'albero richiederebbe 2,6x10 ^ 4214 byte se ogni nodo è un byte (che non sarà)
Esben Skov Pedersen

-3

Non puoi attraversare una struttura di dati a forma di albero senza ricorrere alla ricorsione - se non usi i frame dello stack e le chiamate di funzione fornite dalla tua lingua, devi fondamentalmente programmare il tuo stack e le tue chiamate di funzione, ed è è improbabile che tu riesca a farlo all'interno della lingua in un modo più efficiente rispetto agli scrittori del compilatore sulla macchina su cui verrebbe eseguito il tuo programma.

Pertanto, evitare la ricorsione per paura di imbattersi nei limiti delle risorse è generalmente errato. A dire il vero , l'ottimizzazione prematura delle risorse è sempre sbagliata, ma in questo caso è probabile che anche se si misura e si conferma che l'utilizzo della memoria è il collo di bottiglia, probabilmente non sarà possibile migliorarlo senza scendere al livello del autore del compilatore.


2
Questo è semplicemente falso. È certamente possibile attraversare un albero senza ricorrere alla ricorsione. Non è nemmeno difficile . Puoi anche farlo in modo più efficiente, in modo piuttosto banale, poiché puoi includere nello stack esplicito solo quante più informazioni di cui hai bisogno per il tuo specifico attraversamento, mentre usando la ricorsione finisci per archiviare più informazioni di quelle di cui hai realmente bisogno in molti casi.
Servito il

2
Questa controversia si presenta ogni tanto qui. Alcuni poster considerano il rotolamento del proprio stack non come una ricorsione, mentre altri sottolineano che stanno semplicemente facendo la stessa cosa in modo esplicito che il runtime farebbe altrimenti in modo implicito. Non ha senso discutere su definizioni come questa.
Kilian Foth,

Come definisci la ricorsione allora? Lo definirei come una funzione che si richiama all'interno della propria definizione. Puoi sicuramente attraversare un albero senza mai farlo, come ho dimostrato nella mia risposta.
Servito il

2
Sono cattivo per godermi l'atto di fare clic sul voto negativo su qualcuno con un punteggio rep così alto? È un piacere così raro su questo sito.
Reactgular,

2
Dai @Mat, è roba da bambini. Potresti non essere d'accordo, come se avessi paura di bombardare un albero troppo profondo, è una preoccupazione ragionevole. Puoi solo dirlo.
Mike Dunlavey
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.