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


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.