Di solito BFS è descritto in modo simile al seguente (da Wikipedia ).
1 procedure BFS(G,start_v):
2 let Q be a queue
3 label start_v as discovered
4 Q.enqueue(start_v)
5 while Q is not empty
6 v = Q.dequeue()
7 if v is the goal:
8 return v
9 for all edges from v to w in G.adjacentEdges(v) do
10 if w is not labeled as discovered:
11 label w as discovered
12 w.parent = v
13 Q.enqueue(w)
Il problema è in qualche modo sottile: si nasconde nella riga 3! La domanda è: quale struttura di dati utilizzeremo per memorizzare quali vertici sono stati scoperti?
La soluzione più semplice è utilizzare un array booleano con una voce per vertice. In questo caso, dobbiamo inizializzare tutti gli elementi dell'array su false
e ciò richiede tempo Θ(|V|) . Questo vale per ogni grafico, anche se non ci sono bordi, quindi non possiamo assumere alcuna relazione tra |V|e |E|e otteniamo un tempo di esecuzione di O(|V|+|E|) .
Possiamo evitare di avere una struttura di dati con Θ(|V|) tempo di inizializzazione? Il nostro primo tentativo potrebbe essere quello di utilizzare un elenco collegato. Tuttavia, ora testare se è stato scoperto un vertice (riga 10) richiede un tempo lineare nel numero di vertici visitati, anziché tempo costante come prima. Ciò significa che il tempo di esecuzione diventa O(|V||E|) , che è molto peggio nel caso peggiore. (Nota che non vogliamo riscriverlo comeO(|E|2) dato che è anche peggio: potrebbe essere cattivo come|V|4 , mentre|V||E|≤|V|3 )
L'uso di un array ridimensionato dinamicamente ci consentirebbe di mantenere ordinato l'elenco, quindi ora le ricerche richiederebbero solo il tempo O(log|V|) ma ciò fornisce comunque un tempo di esecuzione di solo O(|E|log|V|) , che è ancora peggio dello standard.
Infine, potremmo usare una tabella hash di dimensioni dinamiche: iniziare con una tabella di dimensioni costanti c e raddoppiarla ogni volta che si riempie per metà. Ciò significa che la dimensione finale della tabella è al massimo il doppio del numero di vertici scoperti prima della fine dell'algoritmo e questo è al massimo |E|+1 perché non scopriamo mai nulla al di fuori del componente del vertice iniziale. Inoltre, la quantità totale di lavoro svolto copiando la tabella hash per espanderla è al massimo c+2c+4c+⋯+2|E|≤4|E|. Le ricerche e gli inserimenti nella tabella hash sono ammortizzati O(1) quindi otteniamo effettivamente un tempo di esecuzione di O(|E|) .
Quindi O(|E|) è possibile, ma vorresti farlo in una vera implementazione? Direi che probabilmente no. A meno che tu non abbia motivo di credere che i tuoi grafici di input avranno molti piccoli componenti, il sovraccarico di mantenere la tabella hash aggiungerà un notevole fattore costante al tempo di esecuzione. La crescita della tabella hash potrebbe richiedere del tempo 4|E|e le ricerche richiederanno di calcolare la funzione hash e, in media, guardare più di uno slot nella tabella. Le scarse prestazioni della cache delle tabelle hash potrebbero anche farti del male su un vero computer. Nella maggior parte dei casi con l'implementazione di array standard, O(|E|)parte è il termine dominante del tempo di esecuzione O(|V|+|E|) quindi non vale la pena usare una tabella hash per rimuovere il termine dominato, dato il costo pratico di farlo.