Я знайшов ще одну різницю між цими підходами. Це виглядає просто і неважливо, але воно має дуже важливу роль, коли ви готуєтесь до інтерв'ю, і ця тема виникає, тому уважно подивіться.
Коротше кажучи: 1) ітераційне проходження після замовлення непросте - це робить DFT складнішим 2) перевірка циклів простіше з рекурсією
Деталі:
У рекурсивному випадку легко створювати попередні та після траверси:
Уявіть собі досить стандартне запитання: "роздрукуйте всі завдання, які слід виконати для виконання завдання 5, коли завдання залежать від інших завдань"
Приклад:
//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!
}
}
Зауважимо, що рекурсивне відстеження після замовлення не потребує подальшого звороту результату. Діти надрукували перше і ваше завдання у запитанні надрукували останнім. Все добре. Ви можете виконати рекурсивний перегляд попереднього замовлення (також показано вище), і для цього знадобиться сторнувати список результатів.
Не так просто з ітераційним підходом! При ітеративному (один стек) підхід ви можете зробити лише попереднє замовлення-обхід, тому ви зобов'язані повернути результат масиву в кінці:
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;
}
Виглядає просто, ні?
Але це пастка в деяких інтерв'ю.
Це означає наступне: за допомогою рекурсивного підходу ви можете реалізувати глибину першої траверси та потім вибрати, яке замовлення вам потрібно попередньо або опублікувати (просто змінивши місце розташування "друку", в нашому випадку "додавання до списку результатів" ). Завдяки ітеративному підходу (один стек) ви можете легко виконати лише попереднє замовлення, і тому в ситуації, коли дітей потрібно надрукувати спочатку (майже всі ситуації, коли вам потрібно почати друкувати з нижніх вузлів, піднімаючись вгору) - ви знаходитесь в біда. Якщо у вас виникли проблеми, ви можете їх змінити пізніше, але це буде доповненням до вашого алгоритму. І якщо інтерв'юер дивиться на годинник, це може бути для вас проблемою. Існують складні способи зробити ітераційне обхід після замовлення, вони існують, але вони є не прості . Приклад:https://www.geeksforgeeks.org/iterative-postorder-traversal-using-stack/
Таким чином, підсумок: я б використовував рекурсію під час співбесіди, простіше керувати та пояснювати. У будь-якому терміновому випадку у вас є простий спосіб перейти від попереднього переходу до замовлення. Ітеративно ви не такі гнучкі.
Я б застосував рекурсію, а потім сказав: "Гаразд, але ітеративний може надати мені більш прямий контроль над використовуваною пам'яттю. Я можу легко виміряти розмір стека та заборонити небезпечне переповнення".
Ще один плюс рекурсії - простіше уникнути / помітити цикли в графіку.
Приклад (предокод):
dft(n){
mark(n)
for(child: n.children){
if(marked(child))
explode - cycle found!!!
dft(child)
}
unmark(n)
}