Ho trovato un'altra differenza tra questi approcci. Sembra semplice e non importante, ma ha un ruolo molto importante mentre ti prepari per le interviste e questo argomento si pone, quindi guarda da vicino.
In breve: 1) l'attraversamento iterativo post-ordine non è facile - questo rende DFT più complesso 2) i cicli controllano più facilmente con la ricorsione
Dettagli:
Nel caso ricorsivo, è facile creare attraversamenti pre e post:
Immagina una domanda piuttosto standard: "stampa tutte le attività che dovrebbero essere eseguite per eseguire l'attività 5, quando le attività dipendono da altre attività"
Esempio:
//key-task, value-list of tasks the key task depends on
//"adjacency map":
Map<Integer, List<Integer>> tasksMap = new HashMap<>();
tasksMap.put(0, new ArrayList<>());
tasksMap.put(1, new ArrayList<>());
List<Integer> t2 = new ArrayList<>();
t2.add(0);
t2.add(1);
tasksMap.put(2, t2);
List<Integer> t3 = new ArrayList<>();
t3.add(2);
t3.add(10);
tasksMap.put(3, t3);
List<Integer> t4 = new ArrayList<>();
t4.add(3);
tasksMap.put(4, t4);
List<Integer> t5 = new ArrayList<>();
t5.add(3);
tasksMap.put(5, t5);
tasksMap.put(6, new ArrayList<>());
tasksMap.put(7, new ArrayList<>());
List<Integer> t8 = new ArrayList<>();
t8.add(5);
tasksMap.put(8, t8);
List<Integer> t9 = new ArrayList<>();
t9.add(4);
tasksMap.put(9, t9);
tasksMap.put(10, new ArrayList<>());
//task to analyze:
int task = 5;
List<Integer> res11 = getTasksInOrderDftReqPostOrder(tasksMap, task);
System.out.println(res11);**//note, no reverse required**
List<Integer> res12 = getTasksInOrderDftReqPreOrder(tasksMap, task);
Collections.reverse(res12);//note reverse!
System.out.println(res12);
private static List<Integer> getTasksInOrderDftReqPreOrder(Map<Integer, List<Integer>> tasksMap, int task) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
reqPreOrder(tasksMap,task,result, visited);
return result;
}
private static void reqPreOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
if(!visited.contains(task)) {
visited.add(task);
result.add(task);//pre order!
List<Integer> children = tasksMap.get(task);
if (children != null && children.size() > 0) {
for (Integer child : children) {
reqPreOrder(tasksMap,child,result, visited);
}
}
}
}
private static List<Integer> getTasksInOrderDftReqPostOrder(Map<Integer, List<Integer>> tasksMap, int task) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
reqPostOrder(tasksMap,task,result, visited);
return result;
}
private static void reqPostOrder(Map<Integer, List<Integer>> tasksMap, int task, List<Integer> result, Set<Integer> visited) {
if(!visited.contains(task)) {
visited.add(task);
List<Integer> children = tasksMap.get(task);
if (children != null && children.size() > 0) {
for (Integer child : children) {
reqPostOrder(tasksMap,child,result, visited);
}
}
result.add(task);//post order!
}
}
Si noti che l'attraversamento post ordine ricorsivo non richiede una successiva inversione del risultato. I bambini hanno stampato per primi e il tuo compito nella domanda è stata stampata per ultima. Va tutto bene. Puoi effettuare un attraversamento pre-ordine ricorsivo (mostrato anche sopra) e richiedere uno storno dell'elenco dei risultati.
Non è così semplice con un approccio iterativo! Nell'approccio iterativo (uno stack) puoi fare solo un pre-order-traversal, quindi hai l'obbligo di invertire l'array dei risultati alla fine:
List<Integer> res1 = getTasksInOrderDftStack(tasksMap, task);
Collections.reverse(res1);//note reverse!
System.out.println(res1);
private static List<Integer> getTasksInOrderDftStack(Map<Integer, List<Integer>> tasksMap, int task) {
List<Integer> result = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
Stack<Integer> st = new Stack<>();
st.add(task);
visited.add(task);
while(!st.isEmpty()){
Integer node = st.pop();
List<Integer> children = tasksMap.get(node);
result.add(node);
if(children!=null && children.size() > 0){
for(Integer child:children){
if(!visited.contains(child)){
st.add(child);
visited.add(child);
}
}
}
//If you put it here - it does not matter - it is anyway a pre-order
//result.add(node);
}
return result;
}
Sembra semplice, no?
Ma è una trappola in alcune interviste.
Significa quanto segue: con l'approccio ricorsivo, è possibile implementare Depth First Traversal e quindi selezionare quale ordine è necessario pre o post (semplicemente modificando la posizione della "stampa", nel nostro caso della "aggiunta all'elenco dei risultati" ). Con l'approccio iterativo (una pila) puoi facilmente eseguire solo il pre-ordine di attraversamento e quindi nella situazione in cui i bambini devono essere stampati per primi (praticamente tutte le situazioni in cui è necessario iniziare a stampare dai nodi inferiori, andando verso l'alto). il problema. Se hai questo problema, puoi invertire in seguito, ma sarà un'aggiunta al tuo algoritmo. E se un intervistatore sta guardando l'orologio, potrebbe essere un problema per te. Esistono modi complessi per eseguire un attraversamento iterativo post-ordine, esistono, ma non sono semplici . Esempio:https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/
Quindi, la linea di fondo: vorrei usare la ricorsione durante le interviste, è più semplice da gestire e da spiegare. Hai un modo semplice per passare dall'attraversamento pre al post ordine in qualsiasi caso urgente. Con iterativo non sei così flessibile.
Vorrei usare la ricorsione e poi dire: "Ok, ma l'iterativo può fornirmi un controllo più diretto sulla memoria utilizzata, posso facilmente misurare le dimensioni dello stack e impedire un pericoloso overflow ..."
Un altro vantaggio della ricorsione: è più semplice evitare / notare cicli in un grafico.
Esempio (preudocodice):
dft(n){
mark(n)
for(child: n.children){
if(marked(child))
explode - cycle found!!!
dft(child)
}
unmark(n)
}