Algoritmo di ricerca della prima profondità non ricorsiva


173

Sto cercando un algoritmo di ricerca di profondità non ricorsivo per un albero non binario. Qualsiasi aiuto è molto apprezzato.


1
@Bart Kiers Un albero in generale, a giudicare dal tag.
biziclop,

13
La prima ricerca della profondità è un algoritmo ricorsivo. Le risposte seguenti esplorano i nodi in modo ricorsivo, non utilizzano lo stack di chiamate del sistema per eseguire la ricorsione e utilizzano invece uno stack esplicito.
Null Set

8
@Null Set No, è solo un ciclo. Secondo la tua definizione, ogni programma per computer è ricorsivo. (Che, in un certo senso della parola sono.)
biziclop,

1
@Null Set: un albero è anche una struttura di dati ricorsiva.
Gumbo,

2
@MuhammadUmer il principale vantaggio degli approcci iterativi rispetto a quelli ricorsivi quando l'iterativo è considerato meno leggibile è che è possibile evitare vincoli di dimensioni massime dello stack / profondità di ricorsione che la maggior parte dei sistemi / linguaggi di programmazione implementano per proteggere lo stack. Con uno stack in memoria il tuo stack è limitato solo dalla quantità di memoria che il tuo programma può consumare, il che in genere consente uno stack molto più grande della dimensione massima dello stack di chiamate.
John B,

Risposte:


313

DFS:

list nodes_to_visit = {root};
while( nodes_to_visit isn't empty ) {
  currentnode = nodes_to_visit.take_first();
  nodes_to_visit.prepend( currentnode.children );
  //do something
}

BFS:

list nodes_to_visit = {root};
while( nodes_to_visit isn't empty ) {
  currentnode = nodes_to_visit.take_first();
  nodes_to_visit.append( currentnode.children );
  //do something
}

La simmetria dei due è piuttosto interessante.

Aggiornamento: come sottolineato, take_first()rimuove e restituisce il primo elemento nell'elenco.


11
+1 per notare quanto sono simili i due quando vengono eseguiti in modo non ricorsivo (come se fossero radicalmente diversi quando sono ricorsivi, ma comunque ...)
corsiKa

3
E quindi per aggiungere alla simmetria, se si utilizza invece una coda con priorità minima come frangia, si dispone di un rilevatore di percorsi più breve a sorgente singola.
Mark Peters,

10
A proposito, la .first()funzione rimuove anche l'elemento dall'elenco. Come shift()in molte lingue. pop()funziona anche e restituisce i nodi figlio nell'ordine da destra a sinistra anziché da sinistra a destra.
Ariel,

5
IMO, l'algo DFS è leggermente errato. Immagina 3 vertici tutti collegati tra loro. Il progresso dovrebbe essere: gray(1st)->gray(2nd)->gray(3rd)->blacken(3rd)->blacken(2nd)->blacken(1st). Ma il codice produce: gray(1st)->gray(2nd)->gray(3rd)->blacken(2nd)->blacken(3rd)->blacken(1st).
Batman,

3
@learner Potrei fraintendere il tuo esempio, ma se sono tutti collegati tra loro, non è proprio un albero.
biziclop,

40

Dovresti usare uno stack che contiene i nodi che non sono stati ancora visitati:

stack.push(root)
while !stack.isEmpty() do
    node = stack.pop()
    for each node.childNodes do
        stack.push(stack)
    endfor
    // …
endwhile

2
@Gumbo Mi chiedo se si tratta di un grafico con cicladi. Può funzionare? Penso di poter semplicemente evitare di aggiungere un nodo minimizzato allo stack e può funzionare. Quello che farò è di contrassegnare tutti i vicini del nodo che sono spuntati e aggiungere un if (nodes are not marked)per giudicare se è opportuno che sia spinto nello stack. Può funzionare?
Alston,

1
@Stallman Potresti ricordare i nodi che hai già visitato. Se poi visiti solo nodi che non hai ancora visitato, non eseguirai alcun ciclo.
Gumbo,

@Gumbo Cosa vuoi dire con doing cycles? Penso di volere solo l'ordine di DFS. È giusto o no, grazie.
Alston,

Volevo solo sottolineare che l'uso di uno stack (LIFO) significa profondità prima traversata. Se si desidera utilizzare breadth-first, utilizzare invece una coda (FIFO).
Per Lundberg,

3
Vale la pena notare che per avere un codice equivalente come la risposta @biziclop più popolare, è necessario inserire le note figlio in ordine inverso ( for each node.childNodes.reverse() do stack.push(stack) endfor). Questo è probabilmente anche quello che vuoi. Bella spiegazione del perché è così in questo video: youtube.com/watch?v=cZPXfl_tUkA endfor
Mariusz Pawelski

32

Se si dispone di puntatori a nodi padre, è possibile farlo senza memoria aggiuntiva.

def dfs(root):
    node = root
    while True:
        visit(node)
        if node.first_child:
            node = node.first_child      # walk down
        else:
            while not node.next_sibling:
                if node is root:
                    return
                node = node.parent       # walk up ...
            node = node.next_sibling     # ... and right

Si noti che se i nodi figlio sono archiviati come array anziché tramite puntatori di pari livello, il prossimo fratello può essere trovato come:

def next_sibling(node):
    try:
        i =    node.parent.child_nodes.index(node)
        return node.parent.child_nodes[i+1]
    except (IndexError, AttributeError):
        return None

Questa è una buona soluzione perché non utilizza memoria aggiuntiva o manipolazione di un elenco o stack (alcuni buoni motivi per evitare la ricorsione). Tuttavia è possibile solo se i nodi dell'albero hanno collegamenti ai loro genitori.
joeytwiddle,

Grazie. Questo algoritmo è eccezionale. Ma in questa versione non è possibile eliminare la memoria del nodo nella funzione visita. Questo algoritmo può convertire l'albero in un elenco a collegamento singolo utilizzando il puntatore "first_child". Quindi puoi percorrerlo e liberare la memoria del nodo senza ricorsione.
puchu,

6
"Se hai puntatori ai nodi principali, puoi farlo senza memoria aggiuntiva": la memorizzazione del puntatore ai nodi principali utilizza un po 'di "memoria aggiuntiva" ...
rptr

1
@ rptr87 se non fosse chiaro, senza memoria aggiuntiva a parte questi puntatori.
Abhinav Gauniyal,

Ciò fallirebbe per alberi parziali in cui il nodo non è la radice assoluta, ma può essere facilmente risolto da while not node.next_sibling or node is root:.
Basilea Shishani,

5

Usa uno stack per tracciare i tuoi nodi

Stack<Node> s;

s.prepend(tree.head);

while(!s.empty) {
    Node n = s.poll_front // gets first node

    // do something with q?

    for each child of n: s.prepend(child)

}

1
@Dave O. No, perché rinvii i figli del nodo visitato di fronte a tutto ciò che è già lì.
biziclop,

Allora ho dovuto interpretare male la semantica di push_back .
Dave O.

@Dave hai un ottimo punto. Stavo pensando che dovrebbe essere "respingere il resto della coda" e non "spingere indietro". Modificherò in modo appropriato.
corsiKa

Se stai spingendo in avanti dovrebbe essere uno stack.
volo

@Timmy sì, non sono sicuro di cosa stavo pensando lì. @quasiverse Normalmente pensiamo a una coda come a una coda FIFO. Uno stack è definito come una coda LIFO.
corsiKa

4

Mentre "usare uno stack" potrebbe funzionare come risposta alla domanda di intervista forzata, in realtà sta facendo esplicitamente ciò che un programma ricorsivo fa dietro le quinte.

La ricorsione utilizza lo stack integrato dei programmi. Quando si chiama una funzione, inserisce gli argomenti nella funzione nello stack e quando la funzione ritorna lo fa facendo scattare lo stack del programma.


7
Con l'importante differenza che lo stack di thread è fortemente limitato e l'algoritmo non ricorsivo utilizzerà l'heap molto più scalabile.
Yam Marcovic,

1
Questa non è solo una situazione inventata. Ho usato tecniche come questa in alcune occasioni in C # e JavaScript per ottenere significativi miglioramenti delle prestazioni rispetto agli equivelanti delle chiamate ricorsive esistenti. Spesso la gestione della ricorsione con uno stack invece di utilizzare lo stack di chiamate è molto più veloce e richiede meno risorse. C'è un sacco di spese generali nel mettere un contesto di chiamata su uno stack e il programmatore è in grado di prendere decisioni pratiche su cosa posizionare su uno stack personalizzato.
Jason Jackson,

4

Un'implementazione di ES6 basata sulla grande risposta di biziclops:

root = {
  text: "root",
  children: [{
    text: "c1",
    children: [{
      text: "c11"
    }, {
      text: "c12"
    }]
  }, {
    text: "c2",
    children: [{
      text: "c21"
    }, {
      text: "c22"
    }]
  }, ]
}

console.log("DFS:")
DFS(root, node => node.children, node => console.log(node.text));

console.log("BFS:")
BFS(root, node => node.children, node => console.log(node.text));

function BFS(root, getChildren, visit) {
  let nodesToVisit = [root];
  while (nodesToVisit.length > 0) {
    const currentNode = nodesToVisit.shift();
    nodesToVisit = [
      ...nodesToVisit,
      ...(getChildren(currentNode) || []),
    ];
    visit(currentNode);
  }
}

function DFS(root, getChildren, visit) {
  let nodesToVisit = [root];
  while (nodesToVisit.length > 0) {
    const currentNode = nodesToVisit.shift();
    nodesToVisit = [
      ...(getChildren(currentNode) || []),
      ...nodesToVisit,
    ];
    visit(currentNode);
  }
}


3
PreOrderTraversal is same as DFS in binary tree. You can do the same recursion 
taking care of Stack as below.

    public void IterativePreOrder(Tree root)
            {
                if (root == null)
                    return;
                Stack s<Tree> = new Stack<Tree>();
                s.Push(root);
                while (s.Count != 0)
                {
                    Tree b = s.Pop();
                    Console.Write(b.Data + " ");
                    if (b.Right != null)
                        s.Push(b.Right);
                    if (b.Left != null)
                        s.Push(b.Left);

                }
            }

La logica generale è, spingere un nodo (a partire dalla radice) nel valore Stack, Pop () e Print (). Quindi se ha figli (sinistro e destro), spingili nello stack - premi prima Destra in modo da visitare prima il figlio Sinistro (dopo aver visitato il nodo stesso). Quando stack è vuoto () avrai visitato tutti i nodi in Preordine.


2

DFS non ricorsivo con generatori ES6

class Node {
  constructor(name, childNodes) {
    this.name = name;
    this.childNodes = childNodes;
    this.visited = false;
  }
}

function *dfs(s) {
  let stack = [];
  stack.push(s);
  stackLoop: while (stack.length) {
    let u = stack[stack.length - 1]; // peek
    if (!u.visited) {
      u.visited = true; // grey - visited
      yield u;
    }

    for (let v of u.childNodes) {
      if (!v.visited) {
        stack.push(v);
        continue stackLoop;
      }
    }

    stack.pop(); // black - all reachable descendants were processed 
  }    
}

Si discosta dal tipico DFS non ricorsivo per rilevare facilmente quando tutti i discendenti raggiungibili di un determinato nodo sono stati elaborati e per mantenere il percorso corrente nell'elenco / stack.


1

Supponiamo di voler eseguire una notifica quando ogni nodo in un grafico viene visitato. La semplice implementazione ricorsiva è:

void DFSRecursive(Node n, Set<Node> visited) {
  visited.add(n);
  for (Node x : neighbors_of(n)) {  // iterate over all neighbors
    if (!visited.contains(x)) {
      DFSRecursive(x, visited);
    }
  }
  OnVisit(n);  // callback to say node is finally visited, after all its non-visited neighbors
}

Ok, ora vuoi un'implementazione basata su stack perché il tuo esempio non funziona. Grafici complessi potrebbero, ad esempio, far esplodere lo stack del programma ed è necessario implementare una versione non ricorsiva. Il problema più grande è sapere quando inviare una notifica.

Il seguente pseudo-codice funziona (mix di Java e C ++ per leggibilità):

void DFS(Node root) {
  Set<Node> visited;
  Set<Node> toNotify;  // nodes we want to notify

  Stack<Node> stack;
  stack.add(root);
  toNotify.add(root);  // we won't pop nodes from this until DFS is done
  while (!stack.empty()) {
    Node current = stack.pop();
    visited.add(current);
    for (Node x : neighbors_of(current)) {
      if (!visited.contains(x)) {
        stack.add(x);
        toNotify.add(x);
      }
    }
  }
  // Now issue notifications. toNotifyStack might contain duplicates (will never
  // happen in a tree but easily happens in a graph)
  Set<Node> notified;
  while (!toNotify.empty()) {
  Node n = toNotify.pop();
  if (!toNotify.contains(n)) {
    OnVisit(n);  // issue callback
    toNotify.add(n);
  }
}

Sembra complicato ma la logica aggiuntiva necessaria per l'emissione delle notifiche esiste perché è necessario notificare in ordine inverso alla visita: DFS inizia da root ma lo notifica per ultimo, a differenza di BFS che è molto semplice da implementare.

Per i calci, prova il seguente grafico: i nodi sono s, t, v e w. i bordi diretti sono: s-> t, s-> v, t-> w, v-> w e v-> t. Esegui la tua implementazione di DFS e l'ordine in cui i nodi dovrebbero essere visitati deve essere: w, t, v, s Un'implementazione goffa di DFS potrebbe notificare t prima e ciò indica un errore. Un'implementazione ricorsiva di DFS avrebbe sempre raggiunto la fine.


1

Codice COMPLETO di esempio WORKING, senza stack:

import java.util.*;

class Graph {
private List<List<Integer>> adj;

Graph(int numOfVertices) {
    this.adj = new ArrayList<>();
    for (int i = 0; i < numOfVertices; ++i)
        adj.add(i, new ArrayList<>());
}

void addEdge(int v, int w) {
    adj.get(v).add(w); // Add w to v's list.
}

void DFS(int v) {
    int nodesToVisitIndex = 0;
    List<Integer> nodesToVisit = new ArrayList<>();
    nodesToVisit.add(v);
    while (nodesToVisitIndex < nodesToVisit.size()) {
        Integer nextChild= nodesToVisit.get(nodesToVisitIndex++);// get the node and mark it as visited node by inc the index over the element.
        for (Integer s : adj.get(nextChild)) {
            if (!nodesToVisit.contains(s)) {
                nodesToVisit.add(nodesToVisitIndex, s);// add the node to the HEAD of the unvisited nodes list.
            }
        }
        System.out.println(nextChild);
    }
}

void BFS(int v) {
    int nodesToVisitIndex = 0;
    List<Integer> nodesToVisit = new ArrayList<>();
    nodesToVisit.add(v);
    while (nodesToVisitIndex < nodesToVisit.size()) {
        Integer nextChild= nodesToVisit.get(nodesToVisitIndex++);// get the node and mark it as visited node by inc the index over the element.
        for (Integer s : adj.get(nextChild)) {
            if (!nodesToVisit.contains(s)) {
                nodesToVisit.add(s);// add the node to the END of the unvisited node list.
            }
        }
        System.out.println(nextChild);
    }
}

public static void main(String args[]) {
    Graph g = new Graph(5);

    g.addEdge(0, 1);
    g.addEdge(0, 2);
    g.addEdge(1, 2);
    g.addEdge(2, 0);
    g.addEdge(2, 3);
    g.addEdge(3, 3);
    g.addEdge(3, 1);
    g.addEdge(3, 4);

    System.out.println("Breadth First Traversal- starting from vertex 2:");
    g.BFS(2);
    System.out.println("Depth First Traversal- starting from vertex 2:");
    g.DFS(2);
}}

output: Breadth First Traversal- a partire dal vertice 2: 2 0 3 1 4 Depth First Traversal- a partire dal vertice 2: 2 3 4 1 0


0

Puoi usare uno stack. Ho implementato grafici con la matrice di adiacenza:

void DFS(int current){
    for(int i=1; i<N; i++) visit_table[i]=false;
    myStack.push(current);
    cout << current << "  ";
    while(!myStack.empty()){
        current = myStack.top();
        for(int i=0; i<N; i++){
            if(AdjMatrix[current][i] == 1){
                if(visit_table[i] == false){ 
                    myStack.push(i);
                    visit_table[i] = true;
                    cout << i << "  ";
                }
                break;
            }
            else if(!myStack.empty())
                myStack.pop();
        }
    }
}

0

DFS iterativo in Java:

//DFS: Iterative
private Boolean DFSIterative(Node root, int target) {
    if (root == null)
        return false;
    Stack<Node> _stack = new Stack<Node>();
    _stack.push(root);
    while (_stack.size() > 0) {
        Node temp = _stack.peek();
        if (temp.data == target)
            return true;
        if (temp.left != null)
            _stack.push(temp.left);
        else if (temp.right != null)
            _stack.push(temp.right);
        else
            _stack.pop();
    }
    return false;
}

La domanda chiede esplicitamente un albero non binario
user3743222

Hai bisogno di una mappa visitata per evitare loop infiniti
spiralmoon,

0

http://www.youtube.com/watch?v=zLZhSSXAwxI

Ho appena visto questo video ed è uscito con l'implementazione. Mi sembra facile da capire. Per favore, critica questo.

visited_node={root}
stack.push(root)
while(!stack.empty){
  unvisited_node = get_unvisited_adj_nodes(stack.top());
  If (unvisited_node!=null){
     stack.push(unvisited_node);  
     visited_node+=unvisited_node;
  }
  else
     stack.pop()
}

0

Utilizzando Stack, ecco i passaggi da seguire: Spingere il primo vertice nello stack quindi,

  1. Se possibile, visitare un vertice non visitato adiacente, contrassegnarlo e spingerlo in pila.
  2. Se non riesci a seguire il passaggio 1, quindi, se possibile, fai uscire un vertice dallo stack.
  3. Se non riesci a seguire il passaggio 1 o il passaggio 2, il gioco è fatto.

Ecco il programma Java seguendo i passaggi precedenti:

public void searchDepthFirst() {
    // begin at vertex 0
    vertexList[0].wasVisited = true;
    displayVertex(0);
    stack.push(0);
    while (!stack.isEmpty()) {
        int adjacentVertex = getAdjacentUnvisitedVertex(stack.peek());
        // if no such vertex
        if (adjacentVertex == -1) {
            stack.pop();
        } else {
            vertexList[adjacentVertex].wasVisited = true;
            // Do something
            stack.push(adjacentVertex);
        }
    }
    // stack is empty, so we're done, reset flags
    for (int j = 0; j < nVerts; j++)
            vertexList[j].wasVisited = false;
}

0
        Stack<Node> stack = new Stack<>();
        stack.add(root);
        while (!stack.isEmpty()) {
            Node node = stack.pop();
            System.out.print(node.getData() + " ");

            Node right = node.getRight();
            if (right != null) {
                stack.push(right);
            }

            Node left = node.getLeft();
            if (left != null) {
                stack.push(left);
            }
        }

0

Pseudo-codice basato sulla risposta di @ biziclop:

  • Utilizzando solo costrutti di base: variabili, array, if, while e for
  • Funzioni getNode(id)egetChildren(id)
  • Supponendo il numero noto di nodi N

NOTA: utilizzo l'indicizzazione di array da 1, non da 0.

Breadth-first

S = Array(N)
S[1] = 1; // root id
cur = 1;
last = 1
while cur <= last
    id = S[cur]
    node = getNode(id)
    children = getChildren(id)

    n = length(children)
    for i = 1..n
        S[ last+i ] = children[i]
    end
    last = last+n
    cur = cur+1

    visit(node)
end

In profondità

S = Array(N)
S[1] = 1; // root id
cur = 1;
while cur > 0
    id = S[cur]
    node = getNode(id)
    children = getChildren(id)

    n = length(children)
    for i = 1..n
        // assuming children are given left-to-right
        S[ cur+i-1 ] = children[ n-i+1 ] 

        // otherwise
        // S[ cur+i-1 ] = children[i] 
    end
    cur = cur+n-1

    visit(node)
end

0

Ecco un link a un programma java che mostra DFS seguendo metodi sia ricorsivi che non ricorsivi e anche calcolando il tempo di scoperta e di fine , ma nessun limite.

    public void DFSIterative() {
    Reset();
    Stack<Vertex> s = new Stack<>();
    for (Vertex v : vertices.values()) {
        if (!v.visited) {
            v.d = ++time;
            v.visited = true;
            s.push(v);
            while (!s.isEmpty()) {
                Vertex u = s.peek();
                s.pop();
                boolean bFinished = true;
                for (Vertex w : u.adj) {
                    if (!w.visited) {
                        w.visited = true;
                        w.d = ++time;
                        w.p = u;
                        s.push(w);
                        bFinished = false;
                        break;
                    }
                }
                if (bFinished) {
                    u.f = ++time;
                    if (u.p != null)
                        s.push(u.p);
                }
            }
        }
    }
}

Fonte completa qui .


0

Volevo solo aggiungere la mia implementazione di Python alla lunga lista di soluzioni. Questo algoritmo non ricorsivo ha eventi di scoperta e terminati.


worklist = [root_node]
visited = set()
while worklist:
    node = worklist[-1]
    if node in visited:
        # Node is finished
        worklist.pop()
    else:
        # Node is discovered
        visited.add(node)
        for child in node.children:
            worklist.append(child)
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.