비 재귀 깊이 우선 검색 알고리즘


173

이진이 아닌 트리에 대한 비 재귀 깊이 우선 검색 알고리즘을 찾고 있습니다. 어떤 도움이라도 대단히 감사합니다.


1
@Bart Kiers 일반적으로 태그로 판단되는 나무입니다.
biziclop

13
깊이 우선 검색은 재귀 알고리즘입니다. 아래의 답변은 재귀 적으로 노드를 탐색하고 재귀를 수행하기 위해 시스템의 호출 스택을 사용하지 않고 대신 명시 적 스택을 사용하고 있습니다.
Null은

8
@Null 설정 아니오, 그것은 단지 루프입니다. 정의에 따라 모든 컴퓨터 프로그램은 재귀 적입니다. (어떤 의미에서 어떤 단어인지)
biziclop

1
@Null 세트 : 트리는 재귀 데이터 구조입니다.
Gumbo

2
@MuhammadUmer 반복이 읽기 어려운 것으로 간주 될 때 재귀 적 접근에 비해 반복의 주요 이점은 대부분의 시스템 / 프로그래밍 언어가 스택을 보호하기 위해 구현하는 최대 스택 크기 / 재귀 깊이 제약을 피할 수 있다는 것입니다. 인 메모리 스택의 경우 스택은 프로그램에서 사용할 수있는 메모리의 양에 의해서만 제한되며 일반적으로 최대 호출 스택 크기보다 훨씬 큰 스택을 허용합니다.
John B

답변:


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
비재

3
그런 다음 대칭에 추가하기 위해 최소 우선 순위 큐를 프린지로 사용하는 경우 단일 소스 최단 경로 파인더가 있습니다.
Mark Peters

10
BTW,이 .first()함수는 또한 목록에서 요소를 제거합니다. shift()많은 언어 에서처럼 . pop()또한 작동하고 왼쪽에서 오른쪽 대신 오른쪽에서 왼쪽 순서로 자식 노드를 반환합니다.
Ariel

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 Cycyles가있는 그래프 인 경우 궁금합니다. 이 작동 할 수 있습니까? 스택에 중복 노드를 추가하는 것을 피할 수 있다고 생각합니다. 내가 할 일은 튀어 나온 노드의 모든 이웃을 표시 if (nodes are not marked)하고 스택에 푸시하기에 적합한 지 여부를 판단 하기 위해를 추가하는 것입니다. 작동 할 수 있습니까?
Alston

1
@Stallman 이미 방문한 노드를 기억할 수 있습니다. 그런 다음 아직 방문하지 않은 노드 만 방문하면주기가 수행되지 않습니다.
Gumbo

@Gumbo 무슨 뜻 doing cycles인가요? 나는 단지 DFS의 순서를 원한다고 생각합니다. 맞습니까? 감사합니다.
Alston

스택 (LIFO)을 사용한다는 것은 깊이 우선 탐색을 의미한다는 점을 지적하고 싶었습니다. 너비 우선을 사용하려면 대신 대기열 (FIFO)을 사용하십시오.
Per Lundberg

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"포인터를 사용하여 트리를 단일 링크 목록으로 변환 할 수 있습니다. 당신이 그것을 통해 재귀없이 노드 메모리를 해제 할 수 있습니다.
puchu

6
"부모 노드에 대한 포인터가있는 경우 추가 메모리없이 수행 할 수 있습니다": 부모 노드에 대한 포인터를 저장하면 "추가 메모리"가 사용됩니다.
rptr

1
명확하지 않은 경우 @ rptr87, 포인터와 별도로 추가 메모리가 없으면.
Abhinav Gauniyal

이것은 노드가 절대 루트가 아니지만 부분적으로 쉽게 수정할 수있는 부분 트리의 경우 실패합니다 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 의 의미를 잘못 해석했을 것 입니다.
Dave O.

@Dave 당신은 아주 좋은 지적이 있습니다. 나는 그것이 "뒤로 밀기"가 아니라 "나머지 줄을 뒤로 밀기"여야한다고 생각했다. 적절하게 편집하겠습니다.
corsiKa

앞쪽으로 밀면 스택이되어야합니다.
비행

@Timmy 그래, 내가 무슨 생각을했는지 모르겠다. @quasiverse 우리는 일반적으로 대기열을 FIFO 대기열이라고 생각합니다. 스택은 LIFO 대기열로 정의됩니다.
corsiKa

4

"스택 사용" 면담 질문에 대한 해답으로 작동 할 수 있지만 실제로 재귀 프로그램이 배후에서하는 일을 명시 적으로 수행하고 있습니다.

재귀는 프로그램 내장 스택을 사용합니다. 함수를 호출하면 함수의 인수를 스택으로 푸시하고 함수가 반환하면 프로그램 스택을 팝핑하여 반환합니다.


7
스레드 스택이 심각하게 제한되고 비 재귀 알고리즘이 훨씬 확장 가능한 힙을 사용한다는 중요한 차이점이 있습니다.
얌 마르코비치

1
이것은 단지 고안된 상황이 아닙니다. C # 및 JavaScript에서 이와 같은 기술을 사용하여 기존 재귀 호출 동등성에 비해 상당한 성능 향상을 얻었습니다. 호출 스택을 사용하는 대신 스택을 사용하여 재귀를 관리하는 것이 훨씬 빠르고 리소스를 덜 사용하는 경우가 종종 있습니다. 프로그래머가 커스텀 스택에 배치 할 것에 대한 실질적인 결정을 내릴 수있는 것과 비교하여 호출 컨텍스트를 스택에 배치하는 데 많은 오버 헤드가 있습니다.
Jason Jackson

4

biziclops에 대한 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);

                }
            }

일반적인 논리는 루트에서 시작하여 노드를 스택으로 밀어 넣고 Pop () 및 Print () 값입니다. 그런 다음 자식이있는 경우 (왼쪽 및 오른쪽) 스택에 밀어 넣으십시오-먼저 오른쪽을 눌러 왼쪽 자식을 먼저 방문하십시오 (노드 자체를 방문한 후). stack이 비면 () 주문 전의 모든 노드를 방문하게됩니다.


2

ES6 생성기를 사용하는 비 재귀 DFS

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

출력 : 정점에서 시작하는 폭 1 차 순회 : 2 0 3 1 4 정점에서 시작하는 폭 1 차 순회 : 2 3 4 1 0


0

스택을 사용할 수 있습니다. Adjacency Matrix를 사용하여 그래프를 구현했습니다.

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

Java에서 DFS 반복 :

//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

무한 루프를 피하기 위해 방문한지도가 필요합니다
spiralmoon

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

참고 : 0이 아닌 1의 배열 색인을 사용합니다.

너비 우선

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

여기 reccursive 비 reccursive 방법도 산출 모두 다음 DFS를 도시하는 Java 프로그램에 대한 링크 인 검색마감 시간이 있지만 에지는 laleling.

    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

방금 긴 솔루션 목록에 파이썬 구현을 추가하고 싶었습니다. 이 비 재귀 알고리즘에는 감지 및 완료된 이벤트가 있습니다.


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)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.