Algorithme de première recherche en profondeur non récursive


173

Je recherche un premier algorithme de recherche en profondeur non récursif pour un arbre non binaire. Toute aide est fortement appréciée.


1
@Bart Kiers Un arbre en général, à en juger par le tag.
biziclop

13
La première recherche en profondeur est un algorithme récursif. Les réponses ci-dessous explorent les nœuds de manière récursive, ils n'utilisent tout simplement pas la pile d'appels du système pour faire leur récursivité, et utilisent plutôt une pile explicite.
Null Set

8
@Null Set Non, c'est juste une boucle. Selon votre définition, chaque programme informatique est récursif. (Ce qui, dans un certain sens du mot, ils sont.)
biziclop

1
@Null Set: Un arbre est également une structure de données récursive.
Gumbo

2
@MuhammadUmer le principal avantage des approches itératives par rapport aux approches récursives lorsque l'itération est considérée comme moins lisible est que vous pouvez éviter les contraintes de taille maximale de pile / profondeur de récursivité que la plupart des systèmes / langages de programmation implémentent pour protéger la pile. Avec une pile en mémoire, votre pile n'est limitée que par la quantité de mémoire que votre programme est autorisé à consommer, ce qui permet généralement une pile beaucoup plus grande que la taille maximale de la pile d'appels.
John B

Réponses:


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 symétrie des deux est assez cool.

Mise à jour: comme indiqué, take_first()supprime et renvoie le premier élément de la liste.


11
+1 pour avoir noté à quel point les deux sont similaires lorsqu'ils sont effectués de manière non récursive (comme s'ils étaient radicalement différents lorsqu'ils sont récursifs, mais quand même ...)
corsiKa

3
Et puis pour ajouter à la symétrie, si vous utilisez une file d'attente de priorité minimale comme frange à la place, vous avez un chercheur de chemin le plus court à source unique.
Mark Peters

10
BTW, la .first()fonction supprime également l'élément de la liste. Comme shift()dans de nombreuses langues. pop()fonctionne également et renvoie les nœuds enfants dans l'ordre de droite à gauche au lieu de gauche à droite.
Ariel

5
OMI, l'algo DFS est légèrement incorrect. Imaginez 3 sommets tous connectés les uns aux autres. Les progrès doivent être: gray(1st)->gray(2nd)->gray(3rd)->blacken(3rd)->blacken(2nd)->blacken(1st). Mais votre code produit: gray(1st)->gray(2nd)->gray(3rd)->blacken(2nd)->blacken(3rd)->blacken(1st).
batman

3
@learner Je comprends peut-être mal votre exemple, mais s'ils sont tous connectés les uns aux autres, ce n'est pas vraiment un arbre.
biziclop

40

Vous utiliseriez une pile contenant les nœuds qui n'ont pas encore été visités:

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

2
@Gumbo Je me demande si c'est un graphique avec des cycyles. Cela peut-il fonctionner? Je pense que je peux simplement éviter d'ajouter un nœud dulpliqué à la pile et cela peut fonctionner. Ce que je vais faire est de marquer tous les voisins du nœud qui sont sortis et d'ajouter un if (nodes are not marked)pour juger s'il est approprié d'être poussé vers la pile. Cela peut-il fonctionner?
Alston

1
@Stallman Vous pouvez vous souvenir des nœuds que vous avez déjà visités. Si vous visitez uniquement les nœuds que vous n'avez pas encore visités, vous ne ferez aucun cycle.
Gumbo

@Gumbo Que voulez-vous dire par doing cycles? Je pense que je veux juste l'ordre de DFS. Est-ce vrai ou pas, merci.
Alston

Je voulais juste souligner que l'utilisation d'une pile (LIFO) signifie une première traversée en profondeur. Si vous voulez utiliser la largeur d'abord, optez plutôt pour une file d'attente (FIFO).
Per Lundberg

3
Il convient de noter que pour avoir un code équivalent à la réponse @biziclop la plus populaire, vous devez pousser les notes enfants dans l'ordre inverse ( for each node.childNodes.reverse() do stack.push(stack) endfor). C'est aussi probablement ce que vous voulez. Belle explication pourquoi c'est comme ça dans cette vidéo: youtube.com/watch?v=cZPXfl_tUkA endfor
Mariusz Pawelski

32

Si vous avez des pointeurs vers des nœuds parents, vous pouvez le faire sans mémoire supplémentaire.

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

Notez que si les nœuds enfants sont stockés sous forme de tableau plutôt que via des pointeurs frères, le frère suivant peut être trouvé sous la forme:

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

C'est une bonne solution car elle n'utilise pas de mémoire supplémentaire ni de manipulation d'une liste ou d'une pile (quelques bonnes raisons d'éviter la récursivité). Cependant, cela n'est possible que si les nœuds de l'arborescence ont des liens vers leurs parents.
joeytwiddle

Je vous remercie. Cet algorithme est génial. Mais dans cette version, vous ne pouvez pas supprimer la mémoire du nœud dans la fonction de visite. Cet algorithme peut convertir l'arborescence en liste à liaison unique en utilisant le pointeur "first_child". Ensuite, vous pouvez le parcourir et libérer la mémoire du nœud sans récursivité.
puchu

6
"Si vous avez des pointeurs vers les nœuds parents, vous pouvez le faire sans mémoire supplémentaire": le stockage du pointeur vers les nœuds parents utilise de la "mémoire supplémentaire" ...
rptr

1
@ rptr87 si ce n'était pas clair, sans mémoire supplémentaire en dehors de ces pointeurs.
Abhinav Gauniyal

Cela échouerait pour les arbres partiels où le nœud n'est pas la racine absolue, mais peut être facilement corrigé par while not node.next_sibling or node is root:.
Basel Shishani

5

Utilisez une pile pour suivre vos nœuds

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. Non, car vous repoussez les enfants du nœud visité devant tout ce qui est déjà là.
biziclop

J'ai dû mal interpréter la sémantique de push_back alors.
Dave O.

@Dave vous avez un très bon point. Je pensais qu'il fallait "repousser le reste de la file d'attente" et non "pousser vers l'arrière". Je vais éditer de manière appropriée.
corsiKa

Si vous poussez vers l'avant, cela devrait être une pile.
vol

@Timmy ouais, je ne sais pas trop à quoi je pensais. @quasiverse Nous considérons normalement une file d'attente comme une file d'attente FIFO. Une pile est définie comme une file d'attente LIFO.
corsiKa

4

Bien que «utiliser une pile» puisse fonctionner comme la réponse à une question d'interview artificielle, en réalité, il s'agit simplement de faire explicitement ce qu'un programme récursif fait dans les coulisses.

La récursivité utilise la pile intégrée des programmes. Lorsque vous appelez une fonction, il pousse les arguments de la fonction sur la pile et lorsque la fonction retourne, il le fait en sautant la pile de programmes.


7
Avec la différence importante que la pile de threads est sévèrement limitée et que l'algorithme non récursif utiliserait le tas beaucoup plus évolutif.
Yam Marcovic

1
Ce n'est pas simplement une situation artificielle. J'ai utilisé des techniques comme celle-ci à quelques reprises en C # et JavaScript pour gagner des performances significatives par rapport aux équivalents d'appel récursifs existants. Il arrive souvent que la gestion de la récursion avec une pile au lieu d'utiliser la pile d'appels soit beaucoup plus rapide et moins gourmande en ressources. Il y a beaucoup de frais généraux impliqués dans le placement d'un contexte d'appel sur une pile par rapport au programmeur étant capable de prendre des décisions pratiques sur ce qu'il faut placer sur une pile personnalisée.
Jason Jackson

4

Une implémentation ES6 basée sur une excellente réponse 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 logique générale est de pousser un nœud (à partir de la racine) dans la valeur Stack, Pop () it et Print (). Ensuite, s'il a des enfants (gauche et droite), poussez-les dans la pile - appuyez d'abord sur Droite pour que vous visitiez d'abord Left child (après avoir visité le nœud lui-même). Lorsque la pile est vide (), vous aurez visité tous les nœuds en pré-commande.


2

DFS non récursif utilisant des générateurs 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 
  }    
}

Il s'écarte du DFS non récursif typique pour détecter facilement quand tous les descendants joignables d'un nœud donné ont été traités et pour maintenir le chemin actuel dans la liste / pile.


1

Supposons que vous souhaitiez exécuter une notification lorsque chaque nœud d'un graphique est visité. L'implémentation récursive simple est:

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, maintenant vous voulez une implémentation basée sur la pile car votre exemple ne fonctionne pas. Des graphes complexes peuvent par exemple faire exploser la pile de votre programme et vous devez implémenter une version non récursive. Le plus gros problème est de savoir quand émettre une notification.

Le pseudo-code suivant fonctionne (mélange de Java et C ++ pour la lisibilité):

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

Cela semble compliqué mais la logique supplémentaire nécessaire pour émettre des notifications existe car vous devez notifier dans l'ordre inverse de la visite - DFS commence à la racine mais le notifie en dernier, contrairement à BFS qui est très simple à implémenter.

Pour les coups de pied, essayez le graphique suivant: les nœuds sont s, t, v et w. les arêtes dirigées sont: s-> t, s-> v, t-> w, v-> w et v-> t. Exécutez votre propre implémentation de DFS et l'ordre dans lequel les nœuds doivent être visités doit être: w, t, v, s Une implémentation maladroite de DFS pourrait peut-être notifier t d'abord et cela indique un bogue. Une implémentation récursive de DFS atteindrait toujours w en dernier.


1

Exemple complet de code de travail, sans pile:

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

sortie: Largeur première traversée - à partir du sommet 2: 2 0 3 1 4 Profondeur première traversée - à partir du sommet 2: 2 3 4 1 0


0

Vous pouvez utiliser une pile. J'ai implémenté des graphiques avec Adjacency Matrix:

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

Itératif DFS en 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 question demande explicitement un arbre non binaire
user3743222

Vous avez besoin d'une carte visitée pour éviter une boucle infinie
spiralmoon

0

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

Je viens de regarder cette vidéo et est sorti avec la mise en œuvre. Cela me semble facile à comprendre. Veuillez critiquer cela.

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

En utilisant Stack, voici les étapes à suivre: Poussez le premier sommet de la pile puis,

  1. Si possible, visitez un sommet non visité adjacent, marquez-le et poussez-le sur la pile.
  2. Si vous ne pouvez pas suivre l'étape 1, alors, si possible, faites sortir un sommet de la pile.
  3. Si vous ne pouvez pas suivre l'étape 1 ou l'étape 2, vous avez terminé.

Voici le programme Java en suivant les étapes ci-dessus:

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-code basé sur la réponse de @ biziclop:

  • En utilisant uniquement des constructions de base: variables, tableaux, if, while et for
  • Fonctions getNode(id)etgetChildren(id)
  • En supposant un nombre connu de nœuds N

REMARQUE: j'utilise l'indexation de tableau à partir de 1, pas de 0.

Largeur d'abord

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

La profondeur d'abord

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

Voici un lien vers un programme java montrant DFS suivant à la fois des méthodes récursives et non récursives et calculant également l' heure de découverte et de fin , mais pas de laleling des bords.

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

Source complète ici .


0

Je voulais juste ajouter mon implémentation python à la longue liste de solutions. Cet algorithme non récursif a des événements de découverte et terminés.


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)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.