如何使用非递归方法实现深度优先搜索图

时间:2014-02-02 08:58:36

标签: algorithm graph depth-first-search non-recursive

好吧,我在这个问题上花了很多时间。但是,我只能找到树的非递归方法的解决方案:Non recursive for tree,或图的递归方法Recursive for graph

许多教程(我不在这里提供这些链接)也没有提供方法。或者教程完全不正确。请帮帮我。

更新

很难形容:

如果我有无向图:

               1
             / |  \
            4  |   2
               3 /

1-- 2-- 3 - 1 是一个循环。

在步骤push the neighbors of the popped vertex into the stack

WHAT'S THE ORDER OF THE VERTEXES SHOULD BE PUSHED?

如果按下的顺序是2 4 3,则堆栈中的顶点是:

| |
|3|
|4|
|2|    
 _

弹出节点后,我们得到了结果:1 - > 3 - > 4 - > 2代替1 - > 3 - > 2 - > 4.

这是不正确的。我应该加入什么条件来阻止这个场景?

13 个答案:

答案 0 :(得分:44)

没有递归的DFS与BFS基本相同 - 但使用stack而不是队列作为数据结构。

线程Iterative DFS vs Recursive DFS and different elements order处理两种方法及它们之间的区别(并且有!你不会以相同的顺序遍历节点!)

迭代方法的算法基本上是:

DFS(source):
  s <- new stack
  visited <- {} // empty set
  s.push(source)
  while (s is not empty):
    current <- s.pop()
    if (current is in visited):
        continue
    visited.add(current)
    // do something with current
    for each node v such that (current,v) is an edge:
        s.push(v)

答案 1 :(得分:16)

这不是一个答案,而是一个扩展的评论,显示该算法在@ amit对当前版本问题中图表的回答中的应用,假设1是起始节点,其邻居按顺序推送2 ,4,3:

               1
             / |  \
            4  |   2
               3 /

Actions            Stack             Visited
=======            =====             =======
push 1             [1]               {}
pop and visit 1    []                {1}
 push 2, 4, 3      [2, 4, 3]         {1}
pop and visit 3    [2, 4]            {1, 3}
 push 1, 2         [2, 4, 1, 2]      {1, 3}
pop and visit 2    [2, 4, 1]         {1, 3, 2}
 push 1, 3         [2, 4, 1, 1, 3]   {1, 3, 2}
pop 3 (visited)    [2, 4, 1, 1]      {1, 3, 2}
pop 1 (visited)    [2, 4, 1]         {1, 3, 2}
pop 1 (visited)    [2, 4]            {1, 3, 2}
pop and visit 4    [2]               {1, 3, 2, 4}
  push 1           [2, 1]            {1, 3, 2, 4}
pop 1 (visited)    [2]               {1, 3, 2, 4}
pop 2 (visited)    []                {1, 3, 2, 4}

因此,应用按顺序2,4,3顺序推送1的邻居的算法导致访问顺序1,3,2,4。无论1的邻居的推送顺序如何,2和3将在访问顺序中相邻,因为首先访问的是另一个尚未访问的对象,以及已访问过的对象。

答案 2 :(得分:6)

DFS逻辑应该是:

1)如果未访问当前节点,请访问节点并将其标记为已访问 2)对于尚未访问的所有邻居,将其推入堆栈

例如,让我们用Java定义一个GraphNode类:

class GraphNode {
    int index;
    ArrayList<GraphNode> neighbors;
}

这里是没有递归的DFS:

void dfs(GraphNode node) {
    // sanity check
    if (node == null) {
        return;
    }

    // use a hash set to mark visited nodes
    Set<GraphNode> set = new HashSet<GraphNode>();

    // use a stack to help depth-first traversal
    Stack<GraphNode> stack = new Stack<GraphNode>();
    stack.push(node);

    while (!stack.isEmpty()) {
        GraphNode curr = stack.pop();

        // current node has not been visited yet
        if (!set.contains(curr)) {
            // visit the node
            // ...

            // mark it as visited
            set.add(curr);
        }

        for (int i = 0; i < curr.neighbors.size(); i++) {
            GraphNode neighbor = curr.neighbors.get(i);

            // this neighbor has not been visited yet
            if (!set.contains(neighbor)) {
                stack.push(neighbor);
            }
        }
    }
}

我们可以使用相同的逻辑递归地执行DFS,克隆图等。

答案 3 :(得分:2)

递归是一种使用调用堆栈来存储图遍历状态的方法。您可以显式使用堆栈,例如通过使用类型为std::stack的局部变量,然后您将不需要递归来实现DFS,而只需要循环。

答案 4 :(得分:2)

好。如果你还在寻找一个java代码

c

答案 5 :(得分:2)

Python代码。时间复杂度为 O V + E ),其中 V E 是分别是顶点和边的数量。空间复杂度为O( V ),因为最坏的情况是存在包含每个顶点而没有任何回溯的路径(即搜索路径是linear chain)。

堆栈存储表单的元组(vertex,vertex_edge_index),以便可以从紧靠该顶点处理的最后一个边之后的边缘处的特定顶点恢复DFS(就像递归的函数调用堆栈一样) DFS)。

示例代码使用complete digraph,其中每个顶点都连接到每个其他顶点。因此,没有必要为每个节点存储显式边缘列表,因为图形是边缘列表(图形 G 包含每个顶点)。

numv = 1000
print('vertices =', numv)
G = [Vertex(i) for i in range(numv)]

def dfs(source):
  s = []
  visited = set()
  s.append((source,None))
  time = 1
  space = 0
  while s:
    time += 1
    current, index = s.pop()
    if index is None:
      visited.add(current)
      index = 0
    # vertex has all edges possible: G is a complete graph
    while index < len(G) and G[index] in visited:
      index += 1
    if index < len(G):
      s.append((current,index+1))
      s.append((G[index], None))
    space = max(space, len(s))
  print('time =', time, '\nspace =', space)

dfs(G[0])

输出:

time = 2000 
space = 1000

请注意,此处的时间是测量 V 操作,而不是 E 。值为 numv * 2,因为每个顶点都被认为是两次,一次是在发现时,一次是在完成时。

答案 6 :(得分:2)

实际上,堆栈不能很好地处理发现时间和完成时间,如果我们想用堆栈实现DFS,并且想要处理发现时间和完成时间,我们需要求助于另一个记录器堆栈,我的实现如下所示,测试正确,下面是case-1,case-2和case-3 graph。

case-1 case-2 case-3

from collections import defaultdict

class Graph(object):

    adj_list = defaultdict(list)

    def __init__(self, V):
        self.V = V

    def add_edge(self,u,v):
        self.adj_list[u].append(v)

    def DFS(self):
        visited = []
        instack = []
        disc = []
        fini = []
        for t in range(self.V):
            visited.append(0)
            disc.append(0)
            fini.append(0)
            instack.append(0)

        time = 0
        for u_ in range(self.V):
            if (visited[u_] != 1):
                stack = []
                stack_recorder = []
                stack.append(u_)
                while stack:
                    u = stack.pop()
                    visited[u] = 1
                    time+=1
                    disc[u] = time
                    print(u)
                    stack_recorder.append(u)
                    flag = 0
                    for v in self.adj_list[u]:
                        if (visited[v] != 1):
                            flag = 1
                            if instack[v]==0:
                                stack.append(v)
                            instack[v]= 1



                    if flag == 0:
                        time+=1
                        temp = stack_recorder.pop()
                        fini[temp] = time
                while stack_recorder:
                    temp = stack_recorder.pop()
                    time+=1
                    fini[temp] = time
        print(disc)
        print(fini)

if __name__ == '__main__':

    V = 6
    G = Graph(V)

#==============================================================================
# #for case 1
#     G.add_edge(0,1)
#     G.add_edge(0,2)
#     G.add_edge(1,3)
#     G.add_edge(2,1)
#     G.add_edge(3,2) 
#==============================================================================

#==============================================================================
# #for case 2
#     G.add_edge(0,1)
#     G.add_edge(0,2)
#     G.add_edge(1,3)
#     G.add_edge(3,2)  
#==============================================================================

#for case 3
    G.add_edge(0,3)    
    G.add_edge(0,1)

    G.add_edge(1,4)
    G.add_edge(2,4)
    G.add_edge(2,5)
    G.add_edge(3,1)
    G.add_edge(4,3)
    G.add_edge(5,5)    


    G.DFS()   

答案 7 :(得分:1)

我认为你需要使用visited[n]布尔数组来检查当前节点是否被访问过。

答案 8 :(得分:1)

很多人会说非递归DFS只是带有堆栈而不是队列的BFS。这不准确,让我再解释一下。

递归DFS

递归DFS使用调用堆栈来保持状态,这意味着您自己不管理单独的堆栈。

但是,对于大型图形,递归DFS(或任何递归函数)可能会导致深度递归,这可能会导致堆栈溢出问题(不是此网站,the real thing)。

非递归DFS

DFS与BFS不同。它具有不同的空间利用率,但如果您像BFS一样实现它,但使用堆栈而不是队列,则将使用比非递归DFS更多的空间。

为什么要有更多空间?

考虑一下:

- (void)drawRect:(CGRect)dirtyRect {
    CGFloat lineWidth = 4 * 2;
    CGRect rect = self.bounds;
    CGFloat side = MIN(rect.size.width, rect.size.height);
    CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));

    UIBezierPath *path = [UIBezierPath bezierPath];
    CGFloat smallRadius = side / 4;
    [path addArcWithCenter:CGPointMake(center.x, center.y - smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:NO];
    [path addArcWithCenter:CGPointMake(center.x, center.y + smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:YES];
    [path addArcWithCenter:center radius:side / 2 startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:NO];
    [path closePath];
    [path setLineJoinStyle:kCGLineJoinRound];
    [path addClip];
    [path setLineWidth:lineWidth];
    [[UIColor whiteColor] setFill];
    [path fill];
    [[UIColor blackColor] setStroke];
    [path stroke];
}

并将其与此进行比较:

// From non-recursive "DFS"
for (auto i&: adjacent) {
    if (!visited(i)) {
        stack.push(i);
    }
}

在第一段代码中,您将所有相邻节点放入堆栈中,然后迭代到下一个相邻顶点并且具有空间成本。如果图表很大,它可以产生显着的差异。

该怎么办?

如果您决定通过在弹出堆栈后再次遍历邻接列表来解决空间问题,那么这将增加时间复杂度成本。

一种解决方案是在您访问它们时逐个添加项目。要实现这一点,您可以在堆栈中保存迭代器,以便在弹出后恢复迭代。

懒惰的方式

在C / C ++中,一种懒惰的方法是使用更大的堆栈大小编译程序并通过// From recursive DFS for (auto i&: adjacent) { if (!visited(i)) { dfs(i); } } 增加堆栈大小,但这真的很糟糕。在Java中,您可以将堆栈大小设置为JVM参数。

答案 9 :(得分:0)

递归算法对于DFS非常有效,因为我们试图尽可能地深入研究,即。一旦我们找到一个未探索的顶点,我们就会立即探索其第一个未探索的邻居。一找到第一个未经探索的邻居,就需要从for循环中断开。

for each neighbor w of v
   if w is not explored
       mark w as explored
       push w onto the stack
       BREAK out of the for loop

答案 10 :(得分:0)

我认为如果我错了,这是一个关于太空纠正我的优化DFS。

s = stack
s.push(initial node)
add initial node to visited
while s is not empty:
    v = s.peek()
    if for all E(v,u) there is one unvisited u:
        mark u as visited
        s.push(u)
    else 
        s.pop

答案 11 :(得分:0)

在递归过程中使用Stack并实现调用堆栈 -

想法是在堆栈中推送一个顶点,然后将它的顶点推到它旁边,该顶点存储在顶点索引处的邻接列表中,然后继续此过程,直到我们无法在图形中进一步移动,现在如果我们不能在图中向前移动那么我们将删除当前位于堆栈顶部的顶点,因为它无法将我们带到任何未访问的顶点。

现在,我们使用堆栈来处理这样一个点,即只有当前顶点可以探索的所有顶点都被访问时才会从堆栈中删除顶点,这是由递归过程自动完成的。

表示前 -

See the example graph here.

(0(1(2(4 4)2)(3 3)1)0)(6(5 5)(7 7)6)

上面的括号显示了顶点在堆栈中添加并从堆栈中删除的顺序,因此只有当可以从中访问的所有顶点都已完成时,才会关闭顶点的括号。

(这里我使用了邻接列表表示,并使用C ++ STL实现为列表向量(vector&gt; AdjList))

void DFSUsingStack() {
    /// we keep a check of the vertices visited, the vector is set to false for all vertices initially.
    vector<bool> visited(AdjList.size(), false);

    stack<int> st;

    for(int i=0 ; i<AdjList.size() ; i++){
        if(visited[i] == true){
            continue;
        }
        st.push(i);
        cout << i << '\n';
        visited[i] = true;
        while(!st.empty()){
            int curr = st.top();
            for(list<int> :: iterator it = AdjList[curr].begin() ; it != AdjList[curr].end() ; it++){
                if(visited[*it] == false){
                    st.push(*it);
                    cout << (*it) << '\n';
                    visited[*it] = true;
                    break;
                }
            }
            /// We can move ahead from current only if a new vertex has been added on the top of the stack.
            if(st.top() != curr){
                continue;
            }
            st.pop();
        }
    }
}

答案 12 :(得分:0)

以下Java代码将非常方便: -

private void DFS(int v,boolean[] visited){
    visited[v]=true;
    Stack<Integer> S = new Stack<Integer>();
    S.push(v);
    while(!S.isEmpty()){
        int v1=S.pop();     
        System.out.println(adjLists.get(v1).name);
        for(Neighbor nbr=adjLists.get(v1).adjList; nbr != null; nbr=nbr.next){
             if (!visited[nbr.VertexNum]){
                 visited[nbr.VertexNum]=true;
                 S.push(nbr.VertexNum);
             }
        }
    }
}
public void dfs() {
    boolean[] visited = new boolean[adjLists.size()];
    for (int v=0; v < visited.length; v++) {
        if (!visited[v])/*This condition is for Unconnected Vertices*/ {

            System.out.println("\nSTARTING AT " + adjLists.get(v).name);
            DFS(v, visited);
        }
    }
}