非再帰的な深さ優先検索アルゴリズム


173

非バイナリツリーの非再帰的な深さ優先検索アルゴリズムを探しています。どんな助けでも大歓迎です。


1
@Bart Kiersタグで判断する一般的なツリー。
biziclop

13
深さ優先検索は再帰的アルゴリズムです。以下の回答はノードを再帰的に探索するものであり、再帰を実行するためにシステムのコールスタックを使用しておらず、代わりに明示的なスタックを使用しています。
空集合

8
@Nullセットいいえ、それは単なるループです。あなたの定義では、すべてのコンピュータープログラムは再帰的です。(どちらかというと、ある意味では、彼らはそうです。)
biziclop

1
@Nullセット:ツリーも再帰的なデータ構造です。
Gumbo

2
@MuhammadUmer反復が可読性が低いと見なされる場合の再帰的アプローチに対する反復の主な利点は、ほとんどのシステム/プログラミング言語がスタックを保護するために実装する最大スタックサイズ/再帰深度の制約を回避できることです。インメモリスタックの場合、スタックは、プログラムが消費を許可されているメモリの量によってのみ制限されます。これにより、通常、最大呼び出しスタックサイズよりはるかに大きなスタックが可能になります。
ジョン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
}

2つの対称性は非常に優れています。

更新:指摘したようtake_first()に、リストの最初の要素を削除して返します。


11
+1は、非再帰的に実行されたときの2つがどれほど似ているか(再帰的なときは根本的に異なるかのようですが、それでも...)
corsiKa

3
さらに対称性を高めるために、代わりに最小優先度キューをフリンジとして使用する場合、単一ソースの最短パスファインダーを使用します。
Mark Peters

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)
バットマン2013

3
@learner私はあなたの例を誤解しているかもしれませんが、それらがすべて互いに接続されている場合、それは実際にはツリーではありません。
biziclop 2013

40

まだ訪問されていないノードを保持するスタックを使用します

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

2
@ガンボそれがcycylesのあるグラフかどうか疑問に思っています。これは機能しますか?スタックに重複ノードを追加することを回避できれば、それは機能します。私が行うことは、ポップアウトされたノードのすべてのネイバーにマークを付けif (nodes are not marked)、スタックにプッシュすることが適切かどうかを判断するためにを追加することです。うまくいきますか?
Alston、2014年

1
@Stallmanすでにアクセスしたノードを思い出すことができます。その後、まだ訪問していないノードのみを訪問する場合、サイクルは実行されません。
ガンボ2014年

@ガンボどういう意味doing cycles?DFSの注文が欲しいだけだと思います。それでいいですか、ありがとうございます。
Alston、2014年

スタック(LIFO)を使用することは、深さ優先トラバーサルを意味することを指摘したかっただけです。幅優先を使用する場合は、代わりにキュー(FIFO)を使用してください。
Lundbergによる

3
最も人気のある@biziclopの回答と同等のコードを作成するには、子ノートを逆の順序でプッシュする必要があることに注意してください(for each node.childNodes.reverse() do stack.push(stack) endfor)。これもおそらくあなたが望むものです。それがこのビデオにある理由は素晴らしい説明です:youtube.com/watch ?v
Mariusz Pawelski 2018

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」ポインタを使用して、ツリーを単一リンクリストに変換できます。再帰せずにウォークスルーしてノードのメモリを解放できます。
ぷちゅ2014

6
「親ノードへのポインターがある場合、追加のメモリなしでそれを行うことができます」:親ノードへのポインターを保存すると、「追加のメモリー」が使用されます...
rptr

1
@ rptr87明確でない場合は、これらのポインターとは別に追加のメモリが必要です。
Abhinav Gauniyal

これは、ノードが絶対ルートではない部分ツリーでは失敗しますが、で簡単に修正できますwhile not node.next_sibling or node is root:
バーゼルシシャニ2017年

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のセマンティクスを誤って解釈したに違いありません。
デイブO.

@デイブあなたは非常に良い点を持っています。それは、「キューの残りの部分を押し戻す」ことであると考えていました。「押し戻す」のではありません。適切に編集します。
corsiKa 2011年

あなたが前に押しているなら、それはスタックでなければなりません。
フライト

@ティミーええ、私はそこで何を考えていたのかわかりません。@quasiverse通常、キューはFIFOキューと見なされます。スタックはLIFOキューとして定義されます。
corsiKa 2011年

4

「スタックを使用する」は、工夫されたインタビューの質問に対する回答として機能する可能性があります、実際には、再帰プログラムが舞台裏で行うことを明示的に行っているだけです。

再帰は、プログラムの組み込みスタックを使用します。関数を呼び出すと、関数の引数がスタックにプッシュされ、関数が戻ると、プログラムスタックがポップされます。


7
スレッドスタックが厳しく制限されており、非再帰的アルゴリズムでははるかにスケーラブルなヒープが使用されるという重要な違いがあります。
Yam Marcovic 2017年

1
これは単なる不自然な状況ではありません。このような手法をC#およびJavaScriptで数回使用して、既存の再帰呼び出しの同等物よりも大幅にパフォーマンスを向上させました。呼び出しスタックを使用する代わりにスタックを使用して再帰を管理する方がはるかに高速でリソース集約型ではない場合がよくあります。スタックに呼び出しコンテキストを配置することには多くのオーバーヘッドがありますが、プログラマーはカスタムスタックに何を配置するかについて実際的な決定を下すことができます。
ジェイソンジャクソン

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()値です。次に、子(左と右)がある場合は、それらをスタックにプッシュします-最初に右をプッシュして、最初に左の子にアクセスします(ノード自体にアクセスした後)。スタックがempty()の場合、事前注文のすべてのノードにアクセスします。


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の独自の実装を実行します。ノードを訪問する順序は次のとおりでなければなりません。DFSの再帰的な実装は常に最後に到達します。


1

完全なWORKINGコードの例、スタックなし:

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

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 2015年

無限ループを回避するために訪問済みマップが必要です
スパイラルムーン

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

これは、再帰的方法と非再帰的方法の両方に従ってDFSを示し、検出時間と終了時間を計算しますが、エッジレーリングは行わないJavaプログラムへのリンクです。

    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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.