@ Bart Kiers Ein Baum im Allgemeinen, gemessen am Tag.
Biziclop
13
Die Tiefensuche ist ein rekursiver Algorithmus. Die folgenden Antworten untersuchen rekursiv Knoten, verwenden einfach nicht den Aufrufstapel des Systems, um ihre Rekursion durchzuführen, und verwenden stattdessen einen expliziten Stapel.
Null Set
8
@Null Set Nein, es ist nur eine Schleife. Nach Ihrer Definition ist jedes Computerprogramm rekursiv. (Was sie in gewissem Sinne sind.)
biziclop
1
@Null Set: Ein Baum ist auch eine rekursive Datenstruktur.
Gumbo
2
@MuhammadUmer Der Hauptvorteil von iterativ gegenüber rekursiven Ansätzen, wenn iterativ als weniger lesbar angesehen wird, besteht darin, dass Sie Einschränkungen für die maximale Stapelgröße / Rekursionstiefe vermeiden können, die die meisten Systeme / Programmiersprachen zum Schutz des Stapels implementieren. Bei einem In-Memory-Stack ist Ihr Stack nur durch die Speichermenge begrenzt, die Ihr Programm verbrauchen darf. Dies ermöglicht normalerweise einen Stack, der viel größer als die maximale Größe des Call-Stacks ist.
+1 für die Feststellung, wie ähnlich die beiden sind, wenn sie nicht rekursiv ausgeführt werden (als ob sie sich radikal unterscheiden, wenn sie rekursiv sind, aber immer noch ...)
corsiKa
3
Und um die Symmetrie zu erhöhen, haben Sie einen Finder mit dem kürzesten Pfad aus einer Hand, wenn Sie stattdessen eine Warteschlange mit minimaler Priorität als Rand verwenden.
Mark Peters
10
Übrigens entfernt die .first()Funktion auch das Element aus der Liste. Wie shift()in vielen Sprachen. pop()funktioniert auch und gibt die untergeordneten Knoten in der Reihenfolge von rechts nach links anstatt von links nach rechts zurück.
Ariel
5
IMO, das DFS-Algo ist etwas falsch. Stellen Sie sich 3 Eckpunkte vor, die alle miteinander verbunden sind. Der Fortschritt sollte sein : gray(1st)->gray(2nd)->gray(3rd)->blacken(3rd)->blacken(2nd)->blacken(1st). Aber Ihr Code erzeugt : gray(1st)->gray(2nd)->gray(3rd)->blacken(2nd)->blacken(3rd)->blacken(1st).
Batman
3
@Learner Ich könnte Ihr Beispiel falsch verstehen, aber wenn sie alle miteinander verbunden sind, ist das nicht wirklich ein Baum.
Biziclop
40
Sie würden einen Stapel verwenden , der die Knoten enthält, die noch nicht besucht wurden:
stack.push(root)
while !stack.isEmpty() do
node = stack.pop()
for each node.childNodes do
stack.push(stack)
endfor
// …
endwhile
@ Gumbo Ich frage mich, ob es ein Diagramm mit Cycyles ist. Kann das funktionieren? Ich denke, ich kann es einfach vermeiden, dem Stapel einen doppelten Knoten hinzuzufügen, und es kann funktionieren. Was ich tun werde, ist, alle Nachbarn des Knotens zu markieren, die herausgesprungen sind, und ein hinzuzufügen, um if (nodes are not marked)zu beurteilen, ob es angemessen ist, auf den Stapel geschoben zu werden. Kann das funktionieren?
Alston
1
@Stallman Sie können sich an die Knoten erinnern, die Sie bereits besucht haben. Wenn Sie dann nur Knoten besuchen, die Sie noch nicht besucht haben, werden Sie keine Zyklen durchführen.
Gumbo
@Gumbo Was meinst du damit doing cycles? Ich denke, ich möchte nur die Reihenfolge der DFS. Ist das richtig oder nicht, danke.
Alston
Ich wollte nur darauf hinweisen, dass die Verwendung eines Stapels (LIFO) die erste Durchquerung der Tiefe bedeutet. Wenn Sie die Breite zuerst verwenden möchten, verwenden Sie stattdessen eine Warteschlange (FIFO).
Per Lundberg
3
Es ist erwähnenswert, dass Sie untergeordnete Notizen in umgekehrter Reihenfolge verschieben müssen, um den entsprechenden Code als beliebteste @ biziclop-Antwort zu erhalten ( for each node.childNodes.reverse() do stack.push(stack) endfor). Dies ist wahrscheinlich auch das, was Sie wollen. Schöne Erklärung, warum es so ist, ist in diesem Video: youtube.com/watch?v=cZPXfl_tUkA endfor
Mariusz Pawelski
32
Wenn Sie Zeiger auf übergeordnete Knoten haben, können Sie dies ohne zusätzlichen Speicher tun.
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
Beachten Sie, dass, wenn die untergeordneten Knoten als Array und nicht über Geschwisterzeiger gespeichert werden, das nächste Geschwister wie folgt gefunden werden kann:
Dies ist eine gute Lösung, da kein zusätzlicher Speicher oder keine Manipulation einer Liste oder eines Stapels verwendet wird (einige gute Gründe, um eine Rekursion zu vermeiden). Es ist jedoch nur möglich, wenn die Baumknoten Links zu ihren Eltern haben.
Joeytwiddle
Danke dir. Dieser Algorithmus ist großartig. In dieser Version können Sie jedoch den Speicher des Knotens in der Besuchsfunktion nicht löschen. Dieser Algorithmus kann den Baum mithilfe des Zeigers "first_child" in eine einfach verknüpfte Liste konvertieren. Dann können Sie es durchlaufen und den Speicher des Knotens ohne Rekursion freigeben.
Puchu
6
"Wenn Sie Zeiger auf übergeordnete Knoten haben, können Sie dies ohne zusätzlichen Speicher tun": Das Speichern des Zeigers auf übergeordnete Knoten verwendet etwas "zusätzlichen Speicher" ...
rptr
1
@ rptr87 wenn es nicht klar war, ohne zusätzlichen Speicher abgesehen von diesen Zeigern.
Abhinav Gauniyal
Dies würde für Teilbäume fehlschlagen, bei denen der Knoten nicht die absolute Wurzel ist, sondern leicht durch behoben werden kann while not node.next_sibling or node is root:.
Basel Shishani
5
Verwenden Sie einen Stapel, um Ihre Knoten zu verfolgen
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)
}
@ Dave O. Nein, weil Sie die Kinder des besuchten Knotens vor alles zurückschieben, was bereits vorhanden ist.
Biziclop
Ich muss dann die Semantik von push_back falsch interpretiert haben .
Dave O.
@ Dave du hast einen sehr guten Punkt. Ich dachte, es sollte "den Rest der Warteschlange zurückschieben" und nicht "nach hinten schieben". Ich werde entsprechend bearbeiten.
CorsiKa
Wenn Sie nach vorne schieben, sollte es ein Stapel sein.
Flug
@Timmy ja ich bin mir nicht sicher was ich da gedacht habe. @quasiverse Normalerweise stellen wir uns eine Warteschlange als FIFO-Warteschlange vor. Ein Stapel ist als LIFO-Warteschlange definiert.
CorsiKa
4
Während „einen Stapel verwenden“ könnte zu gekünstelt Interview - Frage als Antwort arbeiten, in Wirklichkeit tut es genau das, was explizit eine rekursive Programm den Kulissen tut hinter sich .
Die Rekursion verwendet den im Programm integrierten Stapel. Wenn Sie eine Funktion aufrufen, werden die Argumente für die Funktion auf den Stapel verschoben. Wenn die Funktion zurückgegeben wird, wird der Programmstapel geöffnet.
Mit dem wichtigen Unterschied, dass der Thread-Stapel stark eingeschränkt ist und der nicht rekursive Algorithmus den viel skalierbareren Heap verwenden würde.
Yam Marcovic
1
Dies ist nicht nur eine erfundene Situation. Ich habe einige Male solche Techniken in C # und JavaScript verwendet, um signifikante Leistungssteigerungen gegenüber vorhandenen rekursiven Aufrufäquivalenzen zu erzielen. Es ist häufig der Fall, dass die Verwaltung der Rekursion mit einem Stapel anstelle der Verwendung des Aufrufstapels viel schneller und weniger ressourcenintensiv ist. Das Platzieren eines Aufrufkontexts auf einem Stapel ist mit einem hohen Aufwand verbunden, da der Programmierer praktische Entscheidungen darüber treffen kann, was auf einem benutzerdefinierten Stapel platziert werden soll.
Jason Jackson
4
Eine ES6-Implementierung basierend auf biziclops gute Antwort:
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);
}
}
Die allgemeine Logik besteht darin, einen Knoten (beginnend mit root) in den Wert Stack, Pop () it und Print () zu verschieben. Wenn dann Kinder (links und rechts) vorhanden sind, schieben Sie diese in den Stapel - drücken Sie zuerst Rechts, damit Sie zuerst das linke Kind besuchen (nachdem Sie den Knoten selbst besucht haben). Wenn der Stapel leer ist (), haben Sie alle Knoten in der Vorbestellung besucht.
classNode{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];// peekif(!u.visited){
u.visited =true;// grey - visitedyield u;}for(let v of u.childNodes){if(!v.visited){
stack.push(v);continue stackLoop;}}
stack.pop();// black - all reachable descendants were processed }}
Es weicht von der typischen nicht rekursiven DFS ab, um leicht zu erkennen, wann alle erreichbaren Nachkommen eines bestimmten Knotens verarbeitet wurden, und um den aktuellen Pfad in der Liste / im Stapel beizubehalten.
Angenommen, Sie möchten eine Benachrichtigung ausführen, wenn jeder Knoten in einem Diagramm besucht wird. Die einfache rekursive Implementierung lautet:
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, jetzt möchten Sie eine stapelbasierte Implementierung, da Ihr Beispiel nicht funktioniert. Komplexe Diagramme können beispielsweise dazu führen, dass der Stapel Ihres Programms zerstört wird und Sie eine nicht rekursive Version implementieren müssen. Das größte Problem besteht darin, zu wissen, wann eine Benachrichtigung ausgegeben werden muss.
Der folgende Pseudocode funktioniert (Mischung aus Java und C ++ zur besseren Lesbarkeit):
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);
}
}
Es sieht kompliziert aus, aber die zusätzliche Logik, die für die Ausgabe von Benachrichtigungen erforderlich ist, ist vorhanden, da Sie in umgekehrter Reihenfolge des Besuchs benachrichtigen müssen. DFS beginnt im Stammverzeichnis, benachrichtigt es jedoch zuletzt, im Gegensatz zu BFS, das sehr einfach zu implementieren ist.
Versuchen Sie für Kicks das folgende Diagramm: Die Knoten sind s, t, v und w. gerichtete Kanten sind: s-> t, s-> v, t-> w, v-> w und v-> t. Führen Sie Ihre eigene Implementierung von DFS aus, und die Reihenfolge, in der Knoten besucht werden sollen, muss lauten: w, t, v, s Eine umständliche Implementierung von DFS würde möglicherweise zuerst t benachrichtigen, was auf einen Fehler hinweist. Eine rekursive Implementierung von DFS würde immer w last erreichen.
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);
}}
Ausgabe: Breite erste Durchquerung - ab Scheitelpunkt 2: 2 0 3 1 4 Tiefe erste Durchquerung - ab Scheitelpunkt 2: 2 3 4 1 0
Habe gerade dieses Video gesehen und bin mit der Implementierung herausgekommen. Es sieht für mich leicht zu verstehen aus. Bitte kritisieren Sie dies.
Pseudocode basierend auf der Antwort von @ biziclop:
Verwenden Sie nur grundlegende Konstrukte: Variablen, Arrays, if, while und for
Funktionen getNode(id)undgetChildren(id)
Angenommene bekannte Anzahl von Knoten N
HINWEIS: Ich verwende die Array-Indizierung von 1, nicht von 0.
Breite zuerst
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
Tiefe zuerst
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
Hier ist ein Link zu einem Java - Programm zeigt DFS folgenden beiden reccursive und nicht-reccursive Methoden und auch die Berechnung Entdeckung und Finish Zeit, aber ohne Rand laleling.
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);
}
}
}
}
}
Ich wollte nur meine Python-Implementierung zur langen Liste der Lösungen hinzufügen. Dieser nicht rekursive Algorithmus verfügt über Erkennungs- und Abschlussereignisse.
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)
Antworten:
DFS:
BFS:
Die Symmetrie der beiden ist ziemlich cool.
Update: Wie bereits erwähnt,
take_first()
wird das erste Element in der Liste entfernt und zurückgegeben.quelle
.first()
Funktion auch das Element aus der Liste. Wieshift()
in vielen Sprachen.pop()
funktioniert auch und gibt die untergeordneten Knoten in der Reihenfolge von rechts nach links anstatt von links nach rechts zurück.gray(1st)->gray(2nd)->gray(3rd)->blacken(3rd)->blacken(2nd)->blacken(1st)
. Aber Ihr Code erzeugt :gray(1st)->gray(2nd)->gray(3rd)->blacken(2nd)->blacken(3rd)->blacken(1st)
.Sie würden einen Stapel verwenden , der die Knoten enthält, die noch nicht besucht wurden:
quelle
if (nodes are not marked)
zu beurteilen, ob es angemessen ist, auf den Stapel geschoben zu werden. Kann das funktionieren?doing cycles
? Ich denke, ich möchte nur die Reihenfolge der DFS. Ist das richtig oder nicht, danke.for each node.childNodes.reverse() do stack.push(stack) endfor
). Dies ist wahrscheinlich auch das, was Sie wollen. Schöne Erklärung, warum es so ist, ist in diesem Video: youtube.com/watch?v=cZPXfl_tUkA endforWenn Sie Zeiger auf übergeordnete Knoten haben, können Sie dies ohne zusätzlichen Speicher tun.
Beachten Sie, dass, wenn die untergeordneten Knoten als Array und nicht über Geschwisterzeiger gespeichert werden, das nächste Geschwister wie folgt gefunden werden kann:
quelle
while not node.next_sibling or node is root:
.Verwenden Sie einen Stapel, um Ihre Knoten zu verfolgen
quelle
Während „einen Stapel verwenden“ könnte zu gekünstelt Interview - Frage als Antwort arbeiten, in Wirklichkeit tut es genau das, was explizit eine rekursive Programm den Kulissen tut hinter sich .
Die Rekursion verwendet den im Programm integrierten Stapel. Wenn Sie eine Funktion aufrufen, werden die Argumente für die Funktion auf den Stapel verschoben. Wenn die Funktion zurückgegeben wird, wird der Programmstapel geöffnet.
quelle
Eine ES6-Implementierung basierend auf biziclops gute Antwort:
quelle
Die allgemeine Logik besteht darin, einen Knoten (beginnend mit root) in den Wert Stack, Pop () it und Print () zu verschieben. Wenn dann Kinder (links und rechts) vorhanden sind, schieben Sie diese in den Stapel - drücken Sie zuerst Rechts, damit Sie zuerst das linke Kind besuchen (nachdem Sie den Knoten selbst besucht haben). Wenn der Stapel leer ist (), haben Sie alle Knoten in der Vorbestellung besucht.
quelle
Nicht rekursives DFS mit ES6-Generatoren
Es weicht von der typischen nicht rekursiven DFS ab, um leicht zu erkennen, wann alle erreichbaren Nachkommen eines bestimmten Knotens verarbeitet wurden, und um den aktuellen Pfad in der Liste / im Stapel beizubehalten.
quelle
Angenommen, Sie möchten eine Benachrichtigung ausführen, wenn jeder Knoten in einem Diagramm besucht wird. Die einfache rekursive Implementierung lautet:
Ok, jetzt möchten Sie eine stapelbasierte Implementierung, da Ihr Beispiel nicht funktioniert. Komplexe Diagramme können beispielsweise dazu führen, dass der Stapel Ihres Programms zerstört wird und Sie eine nicht rekursive Version implementieren müssen. Das größte Problem besteht darin, zu wissen, wann eine Benachrichtigung ausgegeben werden muss.
Der folgende Pseudocode funktioniert (Mischung aus Java und C ++ zur besseren Lesbarkeit):
Es sieht kompliziert aus, aber die zusätzliche Logik, die für die Ausgabe von Benachrichtigungen erforderlich ist, ist vorhanden, da Sie in umgekehrter Reihenfolge des Besuchs benachrichtigen müssen. DFS beginnt im Stammverzeichnis, benachrichtigt es jedoch zuletzt, im Gegensatz zu BFS, das sehr einfach zu implementieren ist.
Versuchen Sie für Kicks das folgende Diagramm: Die Knoten sind s, t, v und w. gerichtete Kanten sind: s-> t, s-> v, t-> w, v-> w und v-> t. Führen Sie Ihre eigene Implementierung von DFS aus, und die Reihenfolge, in der Knoten besucht werden sollen, muss lauten: w, t, v, s Eine umständliche Implementierung von DFS würde möglicherweise zuerst t benachrichtigen, was auf einen Fehler hinweist. Eine rekursive Implementierung von DFS würde immer w last erreichen.
quelle
VOLLES Beispiel ARBEITSCODE, ohne Stapel:
Ausgabe: Breite erste Durchquerung - ab Scheitelpunkt 2: 2 0 3 1 4 Tiefe erste Durchquerung - ab Scheitelpunkt 2: 2 3 4 1 0
quelle
Sie können einen Stapel verwenden. Ich habe Diagramme mit Adjacency Matrix implementiert:
quelle
DFS iterativ in Java:
quelle
http://www.youtube.com/watch?v=zLZhSSXAwxI
Habe gerade dieses Video gesehen und bin mit der Implementierung herausgekommen. Es sieht für mich leicht zu verstehen aus. Bitte kritisieren Sie dies.
quelle
Mit
Stack
den folgenden Schritten können Sie Folgendes ausführen: Schieben Sie dann den ersten Scheitelpunkt auf den Stapel.Hier ist das Java-Programm, das die obigen Schritte ausführt:
quelle
quelle
Pseudocode basierend auf der Antwort von @ biziclop:
getNode(id)
undgetChildren(id)
N
Breite zuerst
Tiefe zuerst
quelle
Hier ist ein Link zu einem Java - Programm zeigt DFS folgenden beiden reccursive und nicht-reccursive Methoden und auch die Berechnung Entdeckung und Finish Zeit, aber ohne Rand laleling.
Vollständige Quelle hier .
quelle
Ich wollte nur meine Python-Implementierung zur langen Liste der Lösungen hinzufügen. Dieser nicht rekursive Algorithmus verfügt über Erkennungs- und Abschlussereignisse.
quelle