使用列表和堆栈在 C# 中实现深度优先搜索
我想创建一个深度优先搜索,我已经在其中取得了一些成功。
这是到目前为止我的代码(除了我的构造函数,请注意 Vertex 和 Edge 类仅包含属性,这里没有什么重要的内容发布):
private Stack<Vertex> workerStack = new Stack<Vertex>();
private List<Vertex> vertices = new List<Vertex>();
private List<Edge> edges = new List<Edge>();
private int numberOfVertices;
private int numberOfClosedVertices;
private int visitNumber = 1;
private void StartSearch()
{
// Make sure to visit all vertices
while (numberOfClosedVertices < numberOfVertices && workerStack.Count > 0)
{
// Get top element in stack and mark it as visited
Vertex workingVertex = workerStack.Pop();
workingVertex.State = State.Visited;
workingVertex.VisitNumber = visitNumber;
visitNumber++;
numberOfClosedVertices++;
// Get all edges connected to the working vertex
foreach (Vertex vertex in GetConnectedVertices(workingVertex))
{
vertex.Parent = workingVertex;
workerStack.Push(vertex);
}
}
}
private List<Vertex> GetConnectedVertices(Vertex vertex)
{
List<Vertex> vertices = new List<Vertex>();
// Get all vertices connected to vertex and is unvisited, then add them to the vertices list
edges.FindAll(edge => edge.VertexSource == vertex && edge.VertexTarget.State == State.Unvisited).ForEach(edge => vertices.Add(edge.VertexTarget));
return vertices;
}
它的工作方式是所有顶点都被访问,但顺序不正确。
以下是我的访问方式与维基百科的比较:
看来我的是转过来的,是从右到左开始的。
你知道是什么原因造成的吗? (对于我的实现的任何建议也将不胜感激)
编辑:我得到了答案,但仍然想显示 GetConnectedVertices 方法的最终结果:
private List<Vertex> GetConnectedVertices(Vertex vertex)
{
List<Vertex> connectingVertices = new List<Vertex>();
(from edge in edges
where edge.VertexSource == vertex && edge.VertexTarget.State == State.Unvisited
select edge).
Reverse().
ToList().
ForEach(edge => connectingVertices.Add(edge.VertexTarget));
return connectingVertices;
}
I want to create a depth first search which I have been somewhat successful in.
Here is my code so far (Except my constructor, note the Vertex and Edge classes only contain properties, nothing important to post here):
private Stack<Vertex> workerStack = new Stack<Vertex>();
private List<Vertex> vertices = new List<Vertex>();
private List<Edge> edges = new List<Edge>();
private int numberOfVertices;
private int numberOfClosedVertices;
private int visitNumber = 1;
private void StartSearch()
{
// Make sure to visit all vertices
while (numberOfClosedVertices < numberOfVertices && workerStack.Count > 0)
{
// Get top element in stack and mark it as visited
Vertex workingVertex = workerStack.Pop();
workingVertex.State = State.Visited;
workingVertex.VisitNumber = visitNumber;
visitNumber++;
numberOfClosedVertices++;
// Get all edges connected to the working vertex
foreach (Vertex vertex in GetConnectedVertices(workingVertex))
{
vertex.Parent = workingVertex;
workerStack.Push(vertex);
}
}
}
private List<Vertex> GetConnectedVertices(Vertex vertex)
{
List<Vertex> vertices = new List<Vertex>();
// Get all vertices connected to vertex and is unvisited, then add them to the vertices list
edges.FindAll(edge => edge.VertexSource == vertex && edge.VertexTarget.State == State.Unvisited).ForEach(edge => vertices.Add(edge.VertexTarget));
return vertices;
}
Its working in the way that all vertices get visited, but not in the right order.
Here is a comparison of how mine gets visited compared to wikipedia:
Its seems mine is turned around and is beginning from right to left.
Do you know what causes it? (Also any advice on my implementation would be greatly appreciated)
EDIT: I got my answer, but still wanted to show the end result for the GetConnectedVertices method:
private List<Vertex> GetConnectedVertices(Vertex vertex)
{
List<Vertex> connectingVertices = new List<Vertex>();
(from edge in edges
where edge.VertexSource == vertex && edge.VertexTarget.State == State.Unvisited
select edge).
Reverse().
ToList().
ForEach(edge => connectingVertices.Add(edge.VertexTarget));
return connectingVertices;
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
正如其他人所指出的,您将按从左到右的顺序推送堆栈上的下一个要访问的节点。这意味着它们会从右到左弹出,因为堆栈颠倒了顺序。堆栈是后进先出的。
您可以通过使 GetConnectedVertices 构建堆栈而不是列表来解决此问题。这样,连接的顶点就会反转两次,一次是当它们进入返回的堆栈时,一次是当它们进入真实的堆栈时。
我想,该实现是有效的,但它有很多基本问题。如果有人向我提供该代码供审查,我会这么说:
首先,假设您想同时对此数据结构进行两次深度优先搜索。要么是因为您在多个线程上执行此操作,要么因为您有一个嵌套循环,其中内循环对与外循环不同的元素执行 DFS。会发生什么?它们相互干扰,因为两者都试图改变“State”和“VisitNumber”字段。像搜索这样应该是“干净”的操作实际上会让你的数据结构变得“脏”,这是一个非常糟糕的主意。
这样做还会使您无法使用持久不可变数据来表示图表的冗余部分。
另外,我注意到您省略了清理代码。 “State”什么时候会恢复到原来的值?如果您进行第二次 DFS 会怎样?由于根已经被访问过,它会立即失败。
出于所有这些原因,更好的选择是将“访问”状态保留在其自己的对象中,而不是保留在每个顶点中。
接下来,为什么所有的状态对象都是类的私有变量呢?这是一个简单的算法;无需为其构建整个类。深度优先搜索算法应该将要搜索的图作为形式参数,而不是对象状态,并且应该根据需要在局部变量而不是字段中维护自己的局部状态。
接下来,图的抽象是……好吧,它不是抽象。它有两个列表,一个是顶点,另一个是边。我们怎么知道这两个列表是一致的?假设存在不在顶点列表中但在边列表中的顶点。你如何防止这种情况发生?你想要的是图形抽象。让图抽象实现担心如何表示边并查找邻居。
接下来,您对 ForEach 的使用既合法又常见,但这让我很头疼。使用所有 lambda 表达式很难阅读代码并对其进行推理。我们有一个非常好的“foreach”语句。使用它。
接下来,您要改变“父”属性,但根本不清楚该属性的用途或为什么它在遍历期间被改变。任意图中的顶点没有“父节点”,除非该图是一棵树,并且如果该图是一棵树,则无需跟踪“已访问”状态;树中没有循环。这是怎么回事?这段代码很奇怪,而且没有必要执行DFS。
接下来,名为 GetConnectedVertices 的辅助方法是一个谎言。它没有连接顶点,而是连接尚未访问的顶点。名称错误的方法非常令人困惑。
最后,这声称是深度优先搜索,但它不搜索任何东西!正在搜索的东西在哪里?返回的结果在哪里?这根本不是搜索,而是遍历。
重新开始。你想要什么? 给定起始顶点的图的深度优先遍历。然后实施它。首先定义您要遍历的内容。一张图表。您需要图表提供什么服务?获取相邻顶点集的一种方法:
您的方法返回什么?按深度优先顺序排列的顶点序列。需要什么?起始顶点。 OK:
我们现在有了深度优先搜索的简单实现;您现在可以使用Where子句:
好的,那么我们如何实现该方法,以便它在不破坏图状态的情况下进行遍历?保持你自己的外部状态:
看看那是多么干净和简短?没有状态突变。不要乱搞边缘列表。没有名字不好的辅助函数。代码实际上按照它所说的那样做了:遍历一个图。
我们还获得了迭代器块的好处;也就是说,如果有人使用它进行 DF 搜索,那么当满足搜索条件时,迭代就会被放弃。如果我们尽早找到结果,就不必进行完整的遍历。
As others have noted, you are pushing the nodes-to-visit-next on the stack in order from left to right. That means they get popped off right-to-left, since a stack reverses the order. Stacks are last-in-first-out.
You can fix the problem by making GetConnectedVertices build a stack, not a list. That way the connected vertices are reversed twice, once when they go on the returned stack and once when they go on the real stack.
The implementation works, I suppose, but it has a great many fundamental problems. If I were presented that code for review, here's what I'd say:
First off, suppose you wanted to do two depth-first searches of this data structure at the same time. Either because you were doing it on multiple threads, or because you have a nested loop in which the inner loop does a DFS for a different element than the outer loop. What happens? They interfere with each other because both try to mutate the "State" and "VisitNumber" fields. It is a really bad idea to have what should be a "clean" operation like searching actually make your data structure "dirty".
Doing so also makes it impossible for you to use persistent immutable data to represent redundant portions of your graph.
Also, I notice that you omit the code that cleans up. When is "State" ever set back to its original value? What if you did a second DFS? It would immediately fail since the root is already visited.
A better choice for all these reasons is to keep the "visited" state in its own object, not in each vertex.
Next, why are all the state objects private variables of a class? This is a simple algorithm; there's no need to build an entire class for it. A depth first search algorithm should take the graph to search as a formal parameter, not as object state, and it should maintain its own local state as necessary in local variables, not fields.
Next, the abstraction of the graph is... well, its not an abstraction. It's two lists, one of vertices and one of edges. How do we know that those two lists are even consistent? Suppose there are vertices that are not in the vertex list but are on the edge list. How do you prevent that? What you want is a graph abstraction. Let the graph abstraction implementation worry about how to represent edges and find neighbours.
Next, your use of ForEach is both legal and common, but it makes my head hurt. It is hard to read your code and reason about it with all the lambdas. We have a perfectly good "foreach" statement. Use it.
Next, you are mutating a "parent" property but it is not at all clear what this property is for or why it is being mutated during a traversal. Vertices in an arbitrary graph do not have "parents" unless the graph is a tree, and if the graph is a tree then there is no need to keep track of the "visited" state; there are no loops in a tree. What is going on here? This code is just bizarre, and it is not necessary to perform a DFS.
Next, your helper method named GetConnectedVertices is a lie. It does not get connected vertices, it gets connected not-already-visited vertices. Methods whose names lie are very confusing.
Finally, this claims to be a depth first search but it doesn't search for anything! Where is the thing being searched for? Where is the result returned? This isn't a search at all, it's a traversal.
Start over. What do you want? A depth-first traversal of a graph given a starting vertex. Then implement that. Start by defining what you are traversing. A graph. What service do you need from a graph? A way of getting the set of neighbouring vertices:
What is your method returning? A sequence of Vertices in depth-first order. What does it take? A starting vertex. OK:
We now have a trivial implementation of depth first search; you can now use the Where clause:
OK, so how are we going to implement that method so it does a traversal without wrecking the graph's state? Maintain your own external state:
See how much cleaner and shorter that is? No mutation of state. No mucking around with edge lists. No badly-named helper functions. And the code actually does what it says it does: traverses a graph.
We also get the benefits of iterator blocks; namely, if someone is using this for a DF search, then the iteration is abandoned when the search criteria are met. We don't have to do a full traversal if we find the result early.
我概括了 @Eric 的任何
T
的 DFS 遍历代码,使之适用于任何有子项的类型 - 我想我会分享:示例用法:
I generalized @Eric's code for DFS traversal for any
T
to make things work for any type that has children - I thought I'd share:Example usage:
问题在于搜索元素的顺序。
StartSearch
中的for every
不保证元素顺序。您也不会在GetConnectedVertices
方法中FindAll
。让我们看一下这一行:您应该添加一个
OrderBy()
以确保所需的顺序。The problem is in the order you search the elements. Your
for each
inStartSearch
does not guarantee element order. Neither does youFindAll
inGetConnectedVertices
method. Let's look at this line:You should add an
OrderBy()
to ensure the desired order.项目将以与压入堆栈相反的顺序从堆栈中弹出:
stach.push() 结果为: 1 2 3 4 5
stack.pop() 结果为: 5 4 3 2 1 (所以:从右开始向左)
Items will be popped of the stack in the reverse order as how they are pushed on it:
stach.push() results in: 1 2 3 4 5
stack.pop() results in: 5 4 3 2 1 (so: from right to left)
你可能会喜欢这个:
You might enjoy this:
这是我的实现,一堆就足够了。在 foreach 循环之前完成相反的操作。
This is my implemenation, one stack is good enough. A reverse is done before the foreach loop.