Supongamos que desea implementar una búsqueda amplia de un árbol binario de forma recursiva . ¿Cómo lo harías?
¿Es posible usar solo la pila de llamadas como almacenamiento auxiliar?
Supongamos que desea implementar una búsqueda amplia de un árbol binario de forma recursiva . ¿Cómo lo harías?
¿Es posible usar solo la pila de llamadas como almacenamiento auxiliar?
Respuestas:
(Supongo que esto es solo un tipo de ejercicio de pensamiento, o incluso una pregunta de tarea / entrevista engañosa, pero supongo que podría imaginar un escenario extraño en el que no se le permite ningún espacio de almacenamiento dinámico por alguna razón [alguna muy mala costumbre administrador de memoria? algunos problemas extraños de tiempo de ejecución / sistema operativo?] mientras todavía tiene acceso a la pila ...)
El primer recorrido de ancho usa tradicionalmente una cola, no una pila. La naturaleza de una cola y una pila son bastante opuestas, por lo que tratar de usar la pila de llamadas (que es una pila, de ahí el nombre) como almacenamiento auxiliar (una cola) está prácticamente condenada al fracaso, a menos que esté haciendo algo estúpidamente ridículo con la pila de llamadas que no deberías ser.
En el mismo token, la naturaleza de cualquier recursión sin cola que intente implementar es esencialmente agregar una pila al algoritmo. Esto hace que ya no se amplíe la primera búsqueda en un árbol binario y, por lo tanto, el tiempo de ejecución y otras cosas para BFS tradicional ya no se aplican por completo. Por supuesto, siempre puede convertir trivialmente cualquier bucle en una llamada recursiva, pero eso no es ningún tipo de recursión significativa.
Sin embargo, hay formas, como lo demuestran otros, para implementar algo que sigue la semántica de BFS a un costo. Si el costo de la comparación es costoso pero el recorrido del nodo es barato, entonces, como lo hizo @Simon Buchan , simplemente puede ejecutar una búsqueda iterativa en profundidad, solo procesando las hojas. Esto significaría que no hay una cola creciente almacenada en el montón, solo una variable de profundidad local, y las pilas que se acumulan una y otra vez en la pila de llamadas a medida que se recorre el árbol una y otra vez. Y como señaló @Patrick , un árbol binario respaldado por una matriz generalmente se almacena en orden transversal primero, por lo que una búsqueda de primer orden sería trivial, también sin necesidad de una cola auxiliar.
Si usa una matriz para respaldar el árbol binario, puede determinar el siguiente nodo algebraicamente. si i
es un nodo, sus hijos se pueden encontrar en 2i + 1
(para el nodo izquierdo) y 2i + 2
(para el nodo derecho). El siguiente vecino de un nodo está dado por i + 1
, a menos que i
sea un poder de2
Aquí hay un pseudocódigo para una implementación muy ingenua de la primera búsqueda de amplitud en un árbol de búsqueda binaria respaldado por matriz. Esto supone una matriz de tamaño fijo y, por lo tanto, un árbol de profundidad fija. Examinará los nodos sin padres y podría crear una pila inmanejablemente grande.
bintree-bfs(bintree, elt, i)
if (i == LENGTH)
return false
else if (bintree[i] == elt)
return true
else
return bintree-bfs(bintree, elt, i+1)
No pude encontrar una manera de hacerlo completamente recursivo (sin ninguna estructura de datos auxiliar). Pero si la cola Q se pasa por referencia, entonces puede tener la siguiente función recursiva de cola tonta:
BFS(Q)
{
if (|Q| > 0)
v <- Dequeue(Q)
Traverse(v)
foreach w in children(v)
Enqueue(Q, w)
BFS(Q)
}
El siguiente método usó un algoritmo DFS para obtener todos los nodos en una profundidad particular, que es lo mismo que hacer BFS para ese nivel. Si descubre la profundidad del árbol y hace esto para todos los niveles, los resultados serán los mismos que los de un BFS.
public void PrintLevelNodes(Tree root, int level) {
if (root != null) {
if (level == 0) {
Console.Write(root.Data);
return;
}
PrintLevelNodes(root.Left, level - 1);
PrintLevelNodes(root.Right, level - 1);
}
}
for (int i = 0; i < depth; i++) {
PrintLevelNodes(root, i);
}
Encontrar la profundidad de un árbol es pan comido:
public int MaxDepth(Tree root) {
if (root == null) {
return 0;
} else {
return Math.Max(MaxDepth(root.Left), MaxDepth(root.Right)) + 1;
}
}
level
sea cero.
Una simple recursión BFS y DFS en Java:
simplemente presione / ofrezca el nodo raíz del árbol en la pila / cola y llame a estas funciones.
public static void breadthFirstSearch(Queue queue) {
if (queue.isEmpty())
return;
Node node = (Node) queue.poll();
System.out.println(node + " ");
if (node.right != null)
queue.offer(node.right);
if (node.left != null)
queue.offer(node.left);
breadthFirstSearch(queue);
}
public static void depthFirstSearch(Stack stack) {
if (stack.isEmpty())
return;
Node node = (Node) stack.pop();
System.out.println(node + " ");
if (node.right != null)
stack.push(node.right);
if (node.left != null)
stack.push(node.left);
depthFirstSearch(stack);
}
Encontré un algoritmo muy hermoso recursivo (incluso funcional) de Breadth-First transversal. No es mi idea, pero creo que debería mencionarse en este tema.
Chris Okasaki explica su algoritmo de numeración más amplio de ICFP 2000 en http://okasaki.blogspot.de/2008/07/breadth-first-numbering-algorithm-in.html muy claramente con solo 3 imágenes.
La implementación Scala de Debasish Ghosh, que encontré en http://debasishg.blogspot.de/2008/09/breadth-first-numbering-okasakis.html , es:
trait Tree[+T]
case class Node[+T](data: T, left: Tree[T], right: Tree[T]) extends Tree[T]
case object E extends Tree[Nothing]
def bfsNumForest[T](i: Int, trees: Queue[Tree[T]]): Queue[Tree[Int]] = {
if (trees.isEmpty) Queue.Empty
else {
trees.dequeue match {
case (E, ts) =>
bfsNumForest(i, ts).enqueue[Tree[Int]](E)
case (Node(d, l, r), ts) =>
val q = ts.enqueue(l, r)
val qq = bfsNumForest(i+1, q)
val (bb, qqq) = qq.dequeue
val (aa, tss) = qqq.dequeue
tss.enqueue[org.dg.collection.BFSNumber.Tree[Int]](Node(i, aa, bb))
}
}
}
def bfsNumTree[T](t: Tree[T]): Tree[Int] = {
val q = Queue.Empty.enqueue[Tree[T]](t)
val qq = bfsNumForest(1, q)
qq.dequeue._1
}
La manera tonta:
template<typename T>
struct Node { Node* left; Node* right; T value; };
template<typename T, typename P>
bool searchNodeDepth(Node<T>* node, Node<T>** result, int depth, P pred) {
if (!node) return false;
if (!depth) {
if (pred(node->value)) {
*result = node;
}
return true;
}
--depth;
searchNodeDepth(node->left, result, depth, pred);
if (!*result)
searchNodeDepth(node->right, result, depth, pred);
return true;
}
template<typename T, typename P>
Node<T>* searchNode(Node<T>* node, P pred) {
Node<T>* result = NULL;
int depth = 0;
while (searchNodeDepth(node, &result, depth, pred) && !result)
++depth;
return result;
}
int main()
{
// a c f
// b e
// d
Node<char*>
a = { NULL, NULL, "A" },
c = { NULL, NULL, "C" },
b = { &a, &c, "B" },
f = { NULL, NULL, "F" },
e = { NULL, &f, "E" },
d = { &b, &e, "D" };
Node<char*>* found = searchNode(&d, [](char* value) -> bool {
printf("%s\n", value);
return !strcmp((char*)value, "F");
});
printf("found: %s\n", found->value);
return 0;
}
Aquí hay una breve solución de Scala :
def bfs(nodes: List[Node]): List[Node] = {
if (nodes.nonEmpty) {
nodes ++ bfs(nodes.flatMap(_.children))
} else {
List.empty
}
}
La idea de utilizar el valor de retorno como acumulador es muy adecuada. Se puede implementar en otros idiomas de manera similar, solo asegúrese de que su función recursiva procese la lista de nodos .
Lista de códigos de prueba (usando el árbol de prueba @marco):
import org.scalatest.FlatSpec
import scala.collection.mutable
class Node(val value: Int) {
private val _children: mutable.ArrayBuffer[Node] = mutable.ArrayBuffer.empty
def add(child: Node): Unit = _children += child
def children = _children.toList
override def toString: String = s"$value"
}
class BfsTestScala extends FlatSpec {
// 1
// / | \
// 2 3 4
// / | | \
// 5 6 7 8
// / | | \
// 9 10 11 12
def tree(): Node = {
val root = new Node(1)
root.add(new Node(2))
root.add(new Node(3))
root.add(new Node(4))
root.children(0).add(new Node(5))
root.children(0).add(new Node(6))
root.children(2).add(new Node(7))
root.children(2).add(new Node(8))
root.children(0).children(0).add(new Node(9))
root.children(0).children(0).add(new Node(10))
root.children(2).children(0).add(new Node(11))
root.children(2).children(0).add(new Node(12))
root
}
def bfs(nodes: List[Node]): List[Node] = {
if (nodes.nonEmpty) {
nodes ++ bfs(nodes.flatMap(_.children))
} else {
List.empty
}
}
"BFS" should "work" in {
println(bfs(List(tree())))
}
}
Salida:
List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
Aquí hay una implementación de Python:
graph = {'A': ['B', 'C'],
'B': ['C', 'D'],
'C': ['D'],
'D': ['C'],
'E': ['F'],
'F': ['C']}
def bfs(paths, goal):
if not paths:
raise StopIteration
new_paths = []
for path in paths:
if path[-1] == goal:
yield path
last = path[-1]
for neighbor in graph[last]:
if neighbor not in path:
new_paths.append(path + [neighbor])
yield from bfs(new_paths, goal)
for path in bfs([['A']], 'D'):
print(path)
Aquí hay una implementación de Scala 2.11.4 de BFS recursivo. He sacrificado la optimización de llamadas de cola por brevedad, pero la versión TCOd es muy similar. Ver también la publicación de @snv .
import scala.collection.immutable.Queue
object RecursiveBfs {
def bfs[A](tree: Tree[A], target: A): Boolean = {
bfs(Queue(tree), target)
}
private def bfs[A](forest: Queue[Tree[A]], target: A): Boolean = {
forest.dequeueOption exists {
case (E, tail) => bfs(tail, target)
case (Node(value, _, _), _) if value == target => true
case (Node(_, l, r), tail) => bfs(tail.enqueue(List(l, r)), target)
}
}
sealed trait Tree[+A]
case class Node[+A](data: A, left: Tree[A], right: Tree[A]) extends Tree[A]
case object E extends Tree[Nothing]
}
Lo siguiente me parece bastante natural, usando Haskell. Iterar recursivamente sobre los niveles del árbol (aquí recopilo nombres en una gran cadena ordenada para mostrar la ruta a través del árbol):
data Node = Node {name :: String, children :: [Node]}
aTree = Node "r" [Node "c1" [Node "gc1" [Node "ggc1" []], Node "gc2" []] , Node "c2" [Node "gc3" []], Node "c3" [] ]
breadthFirstOrder x = levelRecurser [x]
where levelRecurser level = if length level == 0
then ""
else concat [name node ++ " " | node <- level] ++ levelRecurser (concat [children node | node <- level])
Aquí hay una implementación de Python transversal recursiva de BFS, trabajando para un gráfico sin ciclo.
def bfs_recursive(level):
'''
@params level: List<Node> containing the node for a specific level.
'''
next_level = []
for node in level:
print(node.value)
for child_node in node.adjency_list:
next_level.append(child_node)
if len(next_level) != 0:
bfs_recursive(next_level)
class Node:
def __init__(self, value):
self.value = value
self.adjency_list = []
Me gustaría agregar mis centavos a la respuesta principal en que si el lenguaje admite algo como generador, bfs se puede hacer de forma recursiva.
Para empezar, la respuesta de @ Tanzelax dice:
El primer recorrido de ancho usa tradicionalmente una cola, no una pila. La naturaleza de una cola y una pila son bastante opuestas, por lo que tratar de usar la pila de llamadas (que es una pila, de ahí el nombre) como almacenamiento auxiliar (una cola) está prácticamente condenada al fracaso
De hecho, la pila de llamadas de función ordinarias no se comportará como una pila normal. Pero la función de generador suspenderá la ejecución de la función, por lo que nos da la oportunidad de producir el siguiente nivel de hijos de nodos sin profundizar en los descendientes más profundos del nodo.
El siguiente código es bfs recursivo en Python.
def bfs(root):
yield root
for n in bfs(root):
for c in n.children:
yield c
La intuición aquí es:
Tuve que implementar un recorrido del montón que sale en un orden BFS. En realidad no es BFS, pero realiza la misma tarea.
private void getNodeValue(Node node, int index, int[] array) {
array[index] = node.value;
index = (index*2)+1;
Node left = node.leftNode;
if (left!=null) getNodeValue(left,index,array);
Node right = node.rightNode;
if (right!=null) getNodeValue(right,index+1,array);
}
public int[] getHeap() {
int[] nodes = new int[size];
getNodeValue(root,0,nodes);
return nodes;
}
Sea v el vértice inicial
Deje G ser el gráfico en cuestión
El siguiente es el pseudocódigo sin usar cola
Initially label v as visited as you start from v
BFS(G,v)
for all adjacent vertices w of v in G:
if vertex w is not visited:
label w as visited
for all adjacent vertices w of v in G:
recursively call BFS(G,w)
BFS para un árbol binario (o n-ario) se puede hacer de forma recursiva sin colas de la siguiente manera (aquí en Java):
public class BreathFirst {
static class Node {
Node(int value) {
this(value, 0);
}
Node(int value, int nChildren) {
this.value = value;
this.children = new Node[nChildren];
}
int value;
Node[] children;
}
static void breathFirst(Node root, Consumer<? super Node> printer) {
boolean keepGoing = true;
for (int level = 0; keepGoing; level++) {
keepGoing = breathFirst(root, printer, level);
}
}
static boolean breathFirst(Node node, Consumer<? super Node> printer, int depth) {
if (depth < 0 || node == null) return false;
if (depth == 0) {
printer.accept(node);
return true;
}
boolean any = false;
for (final Node child : node.children) {
any |= breathFirst(child, printer, depth - 1);
}
return any;
}
}
Un ejemplo de impresión transversal números 1-12 en orden ascendente:
public static void main(String... args) {
// 1
// / | \
// 2 3 4
// / | | \
// 5 6 7 8
// / | | \
// 9 10 11 12
Node root = new Node(1, 3);
root.children[0] = new Node(2, 2);
root.children[1] = new Node(3);
root.children[2] = new Node(4, 2);
root.children[0].children[0] = new Node(5, 2);
root.children[0].children[1] = new Node(6);
root.children[2].children[0] = new Node(7, 2);
root.children[2].children[1] = new Node(8);
root.children[0].children[0].children[0] = new Node(9);
root.children[0].children[0].children[1] = new Node(10);
root.children[2].children[0].children[0] = new Node(11);
root.children[2].children[0].children[1] = new Node(12);
breathFirst(root, n -> System.out.println(n.value));
}
#include <bits/stdc++.h>
using namespace std;
#define Max 1000
vector <int> adj[Max];
bool visited[Max];
void bfs_recursion_utils(queue<int>& Q) {
while(!Q.empty()) {
int u = Q.front();
visited[u] = true;
cout << u << endl;
Q.pop();
for(int i = 0; i < (int)adj[u].size(); ++i) {
int v = adj[u][i];
if(!visited[v])
Q.push(v), visited[v] = true;
}
bfs_recursion_utils(Q);
}
}
void bfs_recursion(int source, queue <int>& Q) {
memset(visited, false, sizeof visited);
Q.push(source);
bfs_recursion_utils(Q);
}
int main(void) {
queue <int> Q;
adj[1].push_back(2);
adj[1].push_back(3);
adj[1].push_back(4);
adj[2].push_back(5);
adj[2].push_back(6);
adj[3].push_back(7);
bfs_recursion(1, Q);
return 0;
}
Aquí hay una implementación de JavaScript que simula Breadth First Traversal con Depth First recursión. Estoy almacenando los valores de nodo en cada profundidad dentro de una matriz, dentro de un hash. Si ya existe un nivel (tenemos una colisión), entonces simplemente avanzamos a la matriz en ese nivel. También podría usar una matriz en lugar de un objeto JavaScript, ya que nuestros niveles son numéricos y pueden servir como índices de matriz. Puede devolver nodos, valores, convertirlos en una lista vinculada o lo que desee. Solo estoy devolviendo valores en aras de la simplicidad.
BinarySearchTree.prototype.breadthFirstRec = function() {
var levels = {};
var traverse = function(current, depth) {
if (!current) return null;
if (!levels[depth]) levels[depth] = [current.value];
else levels[depth].push(current.value);
traverse(current.left, depth + 1);
traverse(current.right, depth + 1);
};
traverse(this.root, 0);
return levels;
};
var bst = new BinarySearchTree();
bst.add(20, 22, 8, 4, 12, 10, 14, 24);
console.log('Recursive Breadth First: ', bst.breadthFirstRec());
/*Recursive Breadth First:
{ '0': [ 20 ],
'1': [ 8, 22 ],
'2': [ 4, 12, 24 ],
'3': [ 10, 14 ] } */
Aquí hay un ejemplo del recorrido transversal de Breadth First utilizando un enfoque iterativo.
BinarySearchTree.prototype.breadthFirst = function() {
var result = '',
queue = [],
current = this.root;
if (!current) return null;
queue.push(current);
while (current = queue.shift()) {
result += current.value + ' ';
current.left && queue.push(current.left);
current.right && queue.push(current.right);
}
return result;
};
console.log('Breadth First: ', bst.breadthFirst());
//Breadth First: 20 8 22 4 12 24 10 14
El siguiente es mi código para la implementación completamente recursiva de la búsqueda de amplitud en primer plano de un gráfico bidireccional sin usar bucle y cola.
public class Graph
{
public int V;
public LinkedList<Integer> adj[];
Graph(int v)
{
V = v;
adj = new LinkedList[v];
for (int i=0; i<v; ++i)
adj[i] = new LinkedList<>();
}
void addEdge(int v,int w)
{
adj[v].add(w);
adj[w].add(v);
}
public LinkedList<Integer> getAdjVerted(int vertex)
{
return adj[vertex];
}
public String toString()
{
String s = "";
for (int i=0;i<adj.length;i++)
{
s = s +"\n"+i +"-->"+ adj[i] ;
}
return s;
}
}
//BFS IMPLEMENTATION
public static void recursiveBFS(Graph graph, int vertex,boolean visited[], boolean isAdjPrinted[])
{
if (!visited[vertex])
{
System.out.print(vertex +" ");
visited[vertex] = true;
}
if(!isAdjPrinted[vertex])
{
isAdjPrinted[vertex] = true;
List<Integer> adjList = graph.getAdjVerted(vertex);
printAdjecent(graph, adjList, visited, 0,isAdjPrinted);
}
}
public static void recursiveBFS(Graph graph, List<Integer> vertexList, boolean visited[], int i, boolean isAdjPrinted[])
{
if (i < vertexList.size())
{
recursiveBFS(graph, vertexList.get(i), visited, isAdjPrinted);
recursiveBFS(graph, vertexList, visited, i+1, isAdjPrinted);
}
}
public static void printAdjecent(Graph graph, List<Integer> list, boolean visited[], int i, boolean isAdjPrinted[])
{
if (i < list.size())
{
if (!visited[list.get(i)])
{
System.out.print(list.get(i)+" ");
visited[list.get(i)] = true;
}
printAdjecent(graph, list, visited, i+1, isAdjPrinted);
}
else
{
recursiveBFS(graph, list, visited, 0, isAdjPrinted);
}
}
Implementación de C # del algoritmo de búsqueda de amplitud recursiva para un árbol binario.
Visualización de datos de árbol binario.
IDictionary<string, string[]> graph = new Dictionary<string, string[]> {
{"A", new [] {"B", "C"}},
{"B", new [] {"D", "E"}},
{"C", new [] {"F", "G"}},
{"E", new [] {"H"}}
};
void Main()
{
var pathFound = BreadthFirstSearch("A", "H", new string[0]);
Console.WriteLine(pathFound); // [A, B, E, H]
var pathNotFound = BreadthFirstSearch("A", "Z", new string[0]);
Console.WriteLine(pathNotFound); // []
}
IEnumerable<string> BreadthFirstSearch(string start, string end, IEnumerable<string> path)
{
if (start == end)
{
return path.Concat(new[] { end });
}
if (!graph.ContainsKey(start)) { return new string[0]; }
return graph[start].SelectMany(letter => BreadthFirstSearch(letter, end, path.Concat(new[] { start })));
}
Si desea que el algoritmo funcione no solo con un árbol binario, sino también con gráficos, lo que puede tener dos y más nodos que apuntan al mismo nodo, debe evitar el ciclo automático manteniendo una lista de nodos ya visitados. La implementación puede ser así.
Visualización de datos gráficos
IDictionary<string, string[]> graph = new Dictionary<string, string[]> {
{"A", new [] {"B", "C"}},
{"B", new [] {"D", "E"}},
{"C", new [] {"F", "G", "E"}},
{"E", new [] {"H"}}
};
void Main()
{
var pathFound = BreadthFirstSearch("A", "H", new string[0], new List<string>());
Console.WriteLine(pathFound); // [A, B, E, H]
var pathNotFound = BreadthFirstSearch("A", "Z", new string[0], new List<string>());
Console.WriteLine(pathNotFound); // []
}
IEnumerable<string> BreadthFirstSearch(string start, string end, IEnumerable<string> path, IList<string> visited)
{
if (start == end)
{
return path.Concat(new[] { end });
}
if (!graph.ContainsKey(start)) { return new string[0]; }
return graph[start].Aggregate(new string[0], (acc, letter) =>
{
if (visited.Contains(letter))
{
return acc;
}
visited.Add(letter);
var result = BreadthFirstSearch(letter, end, path.Concat(new[] { start }), visited);
return acc.Concat(result).ToArray();
});
}
Hice un programa usando c ++ que también funciona en gráficos conjuntos y disjuntos.
#include <queue>
#include "iostream"
#include "vector"
#include "queue"
using namespace std;
struct Edge {
int source,destination;
};
class Graph{
int V;
vector<vector<int>> adjList;
public:
Graph(vector<Edge> edges,int V){
this->V = V;
adjList.resize(V);
for(auto i : edges){
adjList[i.source].push_back(i.destination);
// adjList[i.destination].push_back(i.source);
}
}
void BFSRecursivelyJoinandDisjointtGraphUtil(vector<bool> &discovered, queue<int> &q);
void BFSRecursivelyJointandDisjointGraph(int s);
void printGraph();
};
void Graph :: printGraph()
{
for (int i = 0; i < this->adjList.size(); i++)
{
cout << i << " -- ";
for (int v : this->adjList[i])
cout <<"->"<< v << " ";
cout << endl;
}
}
void Graph ::BFSRecursivelyJoinandDisjointtGraphUtil(vector<bool> &discovered, queue<int> &q) {
if (q.empty())
return;
int v = q.front();
q.pop();
cout << v <<" ";
for (int u : this->adjList[v])
{
if (!discovered[u])
{
discovered[u] = true;
q.push(u);
}
}
BFSRecursivelyJoinandDisjointtGraphUtil(discovered, q);
}
void Graph ::BFSRecursivelyJointandDisjointGraph(int s) {
vector<bool> discovered(V, false);
queue<int> q;
for (int i = s; i < V; i++) {
if (discovered[i] == false)
{
discovered[i] = true;
q.push(i);
BFSRecursivelyJoinandDisjointtGraphUtil(discovered, q);
}
}
}
int main()
{
vector<Edge> edges =
{
{0, 1}, {0, 2}, {1, 2}, {2, 0}, {2,3},{3,3}
};
int V = 4;
Graph graph(edges, V);
// graph.printGraph();
graph.BFSRecursivelyJointandDisjointGraph(2);
cout << "\n";
edges = {
{0,4},{1,2},{1,3},{1,4},{2,3},{3,4}
};
Graph graph2(edges,5);
graph2.BFSRecursivelyJointandDisjointGraph(0);
return 0;
}