Нерекурсивний алгоритм першого глибини пошуку


173

Я шукаю алгоритм пошуку першого нерекурсивного глибини для небінарного дерева. Будь-яка допомога дуже цінується.


1
@Bart Kiers Дерево взагалі, судячи з тегу.
biziclop

13
Перший глибинний пошук - це рекурсивний алгоритм. Наведені нижче відповіді рекурсивно вивчають вузли, вони просто не використовують стек виклику системи, щоб зробити свою рекурсію, а замість цього використовується явний стек.
Нульовий сет

8
@Null Set Ні, це просто цикл. За вашим визначенням, кожна комп'ютерна програма є рекурсивною. (Що в певному значенні цього слова вони є.)
biziclop

1
@Null Set: Дерево також є рекурсивною структурою даних.
Gumbo

2
@MuhammadUmer Основна перевага ітеративного над рекурсивними підходами, коли ітератив вважається менш читабельним, полягає в тому, що ви можете уникнути максимальних обмежень розміру стеки / глибини рекурсії, які застосовує більшість систем / мов програмування для захисту стека. За допомогою стека пам'яті ваш стек обмежений лише кількістю пам'яті, яку ваша програма може споживати, що, як правило, дозволяє стек набагато перевищувати максимальний розмір стека виклику.
Джон Б

Відповіді:


313

DFS:

list nodes_to_visit = {root};
while( nodes_to_visit isn't empty ) {
  currentnode = nodes_to_visit.take_first();
  nodes_to_visit.prepend( currentnode.children );
  //do something
}

BFS:

list nodes_to_visit = {root};
while( nodes_to_visit isn't empty ) {
  currentnode = nodes_to_visit.take_first();
  nodes_to_visit.append( currentnode.children );
  //do something
}

Симетрія цих двох досить класна.

Оновлення: як зазначалося, take_first()вилучає та повертає перший елемент у списку.


11
+1 за те, що зауважують, наскільки вони схожі, коли їх виконують не рекурсивно (наче вони кардинально відрізняються, коли вони рекурсивні, але все ж ...)
corsiKa

3
А потім, щоб додати до симетрії, якщо ви використовуєте мінімальну чергу пріоритетів як бахрому, у вас є однонайменний найкоротший пошук шляху.
Марк Петерс

10
До речі, .first()функція також видаляє елемент зі списку. Як і shift()в багатьох мовах. pop()також працює і повертає дочірні вузли в порядку справа наліво замість зліва направо.
Аріель

5
IMO, альго DFS трохи невірно. Уявіть 3 вершини, всі з'єднані між собою. Прогрес має бути: gray(1st)->gray(2nd)->gray(3rd)->blacken(3rd)->blacken(2nd)->blacken(1st). Але ваш код виробляє: gray(1st)->gray(2nd)->gray(3rd)->blacken(2nd)->blacken(3rd)->blacken(1st).
Бетмен

3
@learner Я можу нерозуміти ваш приклад, але якщо вони всі пов’язані між собою, це насправді не дерево.
biziclop

40

Ви б використовували стек, який містить вузли, які ще не були відвідані:

stack.push(root)
while !stack.isEmpty() do
    node = stack.pop()
    for each node.childNodes do
        stack.push(stack)
    endfor
    // …
endwhile

2
@Gumbo Мені цікаво, що якщо це графік з цицилами. Чи може це працювати? Я думаю, що я можу просто уникнути додавання подвійного вузла до стеку, і він може працювати. Що я зроблю, це позначити всіх сусідів вузла, які вискакували, і додати, if (nodes are not marked)щоб судити, чи підходить його натиснення до стека. Це може спрацювати?
Елстон

1
@Stallman Ви можете запам'ятати вузли, які ви вже відвідали. Якщо ви відвідуєте лише ті вузли, яких ви ще не відвідували, ви не зробите жодних циклів.
Gumbo

@Gumbo Що ти маєш на увазі під собою doing cycles? Я думаю, що я просто хочу розпорядження ДФС. Це правильно чи ні, дякую.
Елстон

Просто хотілося зазначити, що використання стека (LIFO) означає першу глибину обходу. Якщо ви хочете скористатися по-перше, перейдіть замість цього в чергу (FIFO).
Пер Лундберг

3
Варто відзначити, що для отримання еквівалентного коду як найпопулярнішої відповіді @biziclop, вам потрібно натиснути дочірні нотатки у зворотному порядку ( for each node.childNodes.reverse() do stack.push(stack) endfor). Це, мабуть, і те, що ви хочете. Приємне пояснення, чому це так, є у цьому відео: youtube.com/watch?v=cZPXfl_tUkA endfor
Mariusz Pawelski

32

Якщо у вас є вказівники на батьківські вузли, ви можете це зробити без додаткової пам'яті.

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

Зауважте, що якщо дочірні вузли зберігаються як масив, а не через покажчики братів, наступний брат може бути знайдений як:

def next_sibling(node):
    try:
        i =    node.parent.child_nodes.index(node)
        return node.parent.child_nodes[i+1]
    except (IndexError, AttributeError):
        return None

Це хороше рішення, оскільки воно не використовує додаткову пам’ять чи маніпулювання списком чи стеком (деякі вагомі причини, щоб уникнути рекурсії). Однак це можливо лише в тому випадку, якщо вузли дерев мають посилання на своїх батьків.
joeytwiddle

Дякую. Цей алгоритм чудовий. Але в цій версії ви не можете видалити пам'ять вузла у функції відвідування. Цей алгоритм може перетворити дерево в односхилий список, використовуючи покажчик "first_child". Чим ви можете пройти по ньому та звільнити пам'ять вузла без рекурсії.
пучу

6
"Якщо у вас є вказівники на батьківські вузли, ви можете це зробити без додаткової пам'яті": для зберігання вказівника на батьківські вузли використовується деяка "додаткова пам'ять" ...
rptr

1
@ rptr87, якщо це було не зрозуміло, без додаткової пам'яті, окрім цих покажчиків.
Абхінав Гауніял

Це не вдасться для часткових дерев, де вузол не є абсолютним коренем, але його можна легко виправити while not node.next_sibling or node is root:.
Базель Шишані

5

Використовуйте стек для відстеження своїх вузлів

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)

}

1
@Dave O. Ні, тому що ти відштовхуєш дітей відвіданого вузла перед усім, що вже є.
biziclop

Я, мабуть, неправильно трактував семантику push_back .
Дейв О.

@Да, у вас дуже хороший момент. Я думав, що слід «відсунути решту черги назад», а не «натиснути назад». Я редагую відповідним чином.
corsiKa

Якщо ви натискаєте на фронт, це повинен бути стек.
рейс

@Timmy Так, я не впевнений, що я там думав. @quasiverse Ми зазвичай думаємо про чергу як чергу FIFO. Стек визначається як черга LIFO.
corsiKa

4

Хоча "використовувати стек" може бути працювати як відповідь на надумане запитання про інтерв'ю, насправді це просто явно робить те, що робить рекурсивна програма за кадром.

Рекурсія використовує вбудований стек програм. Коли ви викликаєте функцію, вона висуває аргументи функції на стек, а коли функція повертається, це робить, вискакуючи стек програми.


7
Маючи важливу відмінність у тому, що стек потоків сильно обмежений, а нерекурсивний алгоритм використовує набагато більш масштабовану купу.
Ям Маркович

1
Це не просто надумана ситуація. Я кілька разів використовував подібні методи в C # та JavaScript, щоб отримати значні підвищення продуктивності в порівнянні з існуючими рекурсивними еквівалентами викликів. Часто буває так, що управління рекурсією стеком замість використання стека викликів набагато швидше та менш ресурсомістке. Для розміщення контексту виклику в стеку є багато накладних витрат, щоб програміст міг приймати практичні рішення щодо того, що розмістити на власному стеку.
Джейсон Джексон

4

Реалізація ES6, заснована на відмінній відповіді:

root = {
  text: "root",
  children: [{
    text: "c1",
    children: [{
      text: "c11"
    }, {
      text: "c12"
    }]
  }, {
    text: "c2",
    children: [{
      text: "c21"
    }, {
      text: "c22"
    }]
  }, ]
}

console.log("DFS:")
DFS(root, node => node.children, node => console.log(node.text));

console.log("BFS:")
BFS(root, node => node.children, node => console.log(node.text));

function BFS(root, getChildren, visit) {
  let nodesToVisit = [root];
  while (nodesToVisit.length > 0) {
    const currentNode = nodesToVisit.shift();
    nodesToVisit = [
      ...nodesToVisit,
      ...(getChildren(currentNode) || []),
    ];
    visit(currentNode);
  }
}

function DFS(root, getChildren, visit) {
  let nodesToVisit = [root];
  while (nodesToVisit.length > 0) {
    const currentNode = nodesToVisit.shift();
    nodesToVisit = [
      ...(getChildren(currentNode) || []),
      ...nodesToVisit,
    ];
    visit(currentNode);
  }
}


3
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);

                }
            }

Загальна логіка полягає в тому, щоб натиснути вузол (починаючи з кореня) у значення Stack, Pop () it та Print (). Потім, якщо у нього є діти (ліворуч і праворуч), натисніть їх у групу - спочатку натисніть праворуч, щоб ви спочатку відвідали Ліву дитину (після відвідування самого вузла). Коли стек порожній (), ви відвідали всі вузли перед попереднім замовленням.


2

Нерекурсивний DFS з використанням генераторів ES6

class Node {
  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]; // peek
    if (!u.visited) {
      u.visited = true; // grey - visited
      yield u;
    }

    for (let v of u.childNodes) {
      if (!v.visited) {
        stack.push(v);
        continue stackLoop;
      }
    }

    stack.pop(); // black - all reachable descendants were processed 
  }    
}

Він відхиляється від типового нерекурсивного DFS, щоб легко виявити, коли всі доступні нащадки даного вузла були оброблені та підтримувати поточний шлях у списку / стеку.


1

Припустимо, ви хочете виконати сповіщення під час відвідування кожного вузла в графі. Проста рекурсивна реалізація:

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
}

Гаразд, тепер ви хочете виконати на основі стека, тому що ваш приклад не працює. Наприклад, складні графіки можуть призвести до того, що це призведе до розбиття стека вашої програми, і вам потрібно реалізувати нерекурсивну версію. Найбільше питання - знати, коли опублікувати повідомлення.

Наступний псевдо-код працює (поєднання Java та C ++ для читабельності):

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);
  }
}

Це виглядає складно, але додаткова логіка, необхідна для видачі сповіщень, існує тому, що потрібно повідомляти в зворотному порядку відвідування - DFS запускається в корені, але повідомляє про це останнім часом, на відміну від BFS, який дуже простий у виконанні.

Для ударів спробуйте наступний графік: вузли s, t, v і w. спрямовані ребра: s-> t, s-> v, t-> w, v-> w і v-> t. Запустіть власну реалізацію DFS і порядок відвідування вузлів повинен бути: w, t, v, s Незграбна реалізація DFS, можливо, спочатку сповістить t, а це вказує на помилку. Рекурсивна реалізація DFS завжди досягала останнього.


1

ПОВНИЙ приклад РОБОЧИЙ код, без стека:

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);
}}

вихід: Перша обробка ширини - починаючи з вершини 2: 2 0 3 1 4 Глибина Перша обробка - починаючи з вершини 2: 2 3 4 1 0


0

Можна використовувати стек. Я реалізував графіки за допомогою матриці суміжності:

void DFS(int current){
    for(int i=1; i<N; i++) visit_table[i]=false;
    myStack.push(current);
    cout << current << "  ";
    while(!myStack.empty()){
        current = myStack.top();
        for(int i=0; i<N; i++){
            if(AdjMatrix[current][i] == 1){
                if(visit_table[i] == false){ 
                    myStack.push(i);
                    visit_table[i] = true;
                    cout << i << "  ";
                }
                break;
            }
            else if(!myStack.empty())
                myStack.pop();
        }
    }
}

0

Ітеративний DFS на Java:

//DFS: Iterative
private Boolean DFSIterative(Node root, int target) {
    if (root == null)
        return false;
    Stack<Node> _stack = new Stack<Node>();
    _stack.push(root);
    while (_stack.size() > 0) {
        Node temp = _stack.peek();
        if (temp.data == target)
            return true;
        if (temp.left != null)
            _stack.push(temp.left);
        else if (temp.right != null)
            _stack.push(temp.right);
        else
            _stack.pop();
    }
    return false;
}

Питання явно запитує не бінарне дерево
user3743222

Вам потрібна відвідана карта, щоб уникнути нескінченного циклу
спіраль місяць

0

http://www.youtube.com/watch?v=zLZhSSXAwxI

Просто подивився це відео і вийшов з реалізацією. Мені це легко зрозуміти. Будь ласка, критикуйте це.

visited_node={root}
stack.push(root)
while(!stack.empty){
  unvisited_node = get_unvisited_adj_nodes(stack.top());
  If (unvisited_node!=null){
     stack.push(unvisited_node);  
     visited_node+=unvisited_node;
  }
  else
     stack.pop()
}

0

Використовуючи Stack, ось наступні кроки: Натисніть першу вершину на стек, потім,

  1. Якщо можливо, відвідайте сусідню небачену вершину, позначте її та натисніть на стек.
  2. Якщо ви не можете виконати крок 1, то, якщо можливо, висуньте вершину зі стека.
  3. Якщо ви не можете виконати крок 1 або крок 2, ви закінчите.

Ось програма Java, виконана вищезазначеними кроками:

public void searchDepthFirst() {
    // begin at vertex 0
    vertexList[0].wasVisited = true;
    displayVertex(0);
    stack.push(0);
    while (!stack.isEmpty()) {
        int adjacentVertex = getAdjacentUnvisitedVertex(stack.peek());
        // if no such vertex
        if (adjacentVertex == -1) {
            stack.pop();
        } else {
            vertexList[adjacentVertex].wasVisited = true;
            // Do something
            stack.push(adjacentVertex);
        }
    }
    // stack is empty, so we're done, reset flags
    for (int j = 0; j < nVerts; j++)
            vertexList[j].wasVisited = false;
}

0
        Stack<Node> stack = new Stack<>();
        stack.add(root);
        while (!stack.isEmpty()) {
            Node node = stack.pop();
            System.out.print(node.getData() + " ");

            Node right = node.getRight();
            if (right != null) {
                stack.push(right);
            }

            Node left = node.getLeft();
            if (left != null) {
                stack.push(left);
            }
        }

0

Псевдокод на основі відповіді @ biziclop:

  • Використання лише основних конструкцій: змінних, масивів, if, while і for
  • Функції getNode(id)таgetChildren(id)
  • Якщо припустити відому кількість вузлів N

ПРИМІТКА. Я використовую індексацію масиву від 1, а не до 0.

Ширина перша

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

Глибина перша

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

0

Ось посилання на java-програму, що показує DFS, що дотримується як рекурсивних, так і нерекурсивних методів, а також обчислює час відкриття та закінчення , але немає межі розміщення.

    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);
                }
            }
        }
    }
}

Повне джерело тут .


0

Просто хотів додати мою реалізацію python до довгого списку рішень. Цей нерекурсивний алгоритм має відкриття та завершені події.


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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.