介绍

图形是存储某些类型的数据的便捷方法。该概念是从数学移植而来的,适合于计算机科学的需求。

由于许多事物可以用图形表示,因此图形遍历已成为一项常见的任务,尤其是在数据科学和机器学习中。

深度优先搜索

深度优先搜索(DFS)沿一个分支尽可能搜索,然后回溯以在下一个分支中尽可能搜索。这意味着,在进行中的Graph中,它从第一个邻居开始,并尽可能沿该行继续下去:

一旦到达该分支的最后一个节点(1),它就会回溯到可能会改变路线(5)的第一个节点,并访问该整个分支(在我们的情况下为节点(2))。

然后它再次回溯到节点(5),并且由于它已经访问了节点(1)和(2),因此它回溯到(3),然后重新路由到下一个分支(8)。

实作

因为我们知道如何通过邻接表和矩阵在代码中表示图,所以让我们制作一个图并使用DFS遍历它。我们将使用的图形非常简单,因此我们选择哪种实现都无关紧要。

但是,对于实际项目,在大多数情况下,邻接表将是一个更好的选择,因此我们将图形表示为邻接表。

我们希望访问所有节点一次,如上面的动画所示,一旦访问它们就会变成红色,因此我们不再访问它们。为此,我们将引入一个visited标志:

public class Node {
int n;
String name;
boolean visited; // New attribute
Node(int n, String name) {
this.n = n;
this.name = name;
visited = false;
}
// Two new methods we'll need in our traversal algorithms
void visit() {
visited = true;
}
void unvisit() {
visited = false;
}
}

现在,让我们定义一个Graph:

public class Graph {
// Each node maps to a list of all his neighbors
private HashMap> adjacencyMap;
private boolean directed;
public Graph(boolean directed) {
this.directed = directed;
adjacencyMap = new HashMap<>();
}
// ...
}

现在,让我们添加方法addEdge()。我们将使用两种方法,辅助方法和实际方法。

在辅助方法中,我们还将检查可能的重复边缘。在A和之间添加边之前B,我们先将其删除,然后再添加。如果边缘已经存在,那么这将阻止我们添加重复的边缘。如果那里还没有边,那么我们在两个节点之间仍然只有一条边。

如果边缘不存在,则删除不存在的边缘将导致,NullPointerException因此我们引入了列表的临时副本:

public void addEdgeHelper(Node a, Node b) {
LinkedList tmp = adjacencyMap.get(a);
if (tmp != null) {
tmp.remove(b);
}
else tmp = new LinkedList<>();
tmp.add(b);
adjacencyMap.put(a, tmp);
}
public void addEdge(Node source, Node destination) {
// We make sure that every used node shows up in our .keySet()
if (!adjacencyMap.keySet().contains(source))
adjacencyMap.put(source, null);
if (!adjacencyMap.keySet().contains(destination))
adjacencyMap.put(destination, null);
addEdgeHelper(source, destination);
// If a graph is undirected, we want to add an edge from destination to source as well
if (!directed) {
addEdgeHelper(destination, source);
}
}

最后,我们将有printEdges(),hasEdge()和resetNodesVisited()辅助方法,这是非常简单的:

public void printEdges() {
for (Node node : adjacencyMap.keySet()) {
System.out.print("The " + node.name + " has an edge towards: ");
for (Node neighbor : adjacencyMap.get(node)) {
System.out.print(neighbor.name + " ");
}
System.out.println();
}
}
public boolean hasEdge(Node source, Node destination) {
return adjacencyMap.containsKey(source) && adjacencyMap.get(source).contains(destination);
}
public void resetNodesVisited(){
for(Node node : adjacencyMap.keySet()){
node.unvisit();
}
}

我们还将添加的depthFirstSearch(Node node)方法,我们的Graph类,它具有下列功能:

如果node.visited == true,只需返回

如果尚未访问过,请执行以下操作:找到第一个未访问的邻居newNode的node和呼叫depthFirstSearch(newNode)

对所有未访问的邻居重复该过程

让我们用一个例子说明一下:

Node A is connected with node D

Node B is connected with nodes D, C

Node C is connected with nodes A, B

Node D is connected with nodes B开始时未访问所有节点(node.visited == false)

.depthFirstSeach()以任意节点作为起始节点进行调用depthFirstSearch(B)

将B标记为已访问

B有没有来访的邻居?是->第一个未访问的节点是D,因此调用depthFirstSearch(D)

将D标记为已访问

D有没有来访的邻居?否->(已访问B)返回

B有没有来访的邻居?是->第一个未访问的节点是C,因此调用depthFirstSearch(C)

将C标记为已访问

C是否有任何未访问的邻居?是->第一个未访问的节点是A,因此调用depthFirstSearch(A)

将A标记为已访问

A有没有来访的邻居?编号->返回

C是否有任何未访问的邻居?否->返回

B有没有来访的邻居?否->返回

在图上调用DFS将给我们遍历B,D,C,A(访问顺序)。这样写出算法后,很容易将其转换为代码:

public void depthFirstSearch(Node node) {
node.visit();
System.out.print(node.name + " ");
LinkedList allNeighbors = adjacencyMap.get(node);
if (allNeighbors == null)
return;
for (Node neighbor : allNeighbors) {
if (!neighbor.isVisited())
depthFirstSearch(neighbor);
}
}

同样,这是转换成动画时的外观:

订阅我们的新闻

在收件箱中获取临时教程,指南和作业。从来没有垃圾邮件。随时退订。

订阅电子报

订阅

DFS有时被称为“激进”图遍历,因为它尽可能地通过一个“分支”。正如我们在上面的gif文件中看到的那样,当DFS遇到节点25时,它将强制25-12-6-4分支,直到无法继续下去为止。只有到那时,算法才从最近访问的节点开始返回以检查先前节点的其他未访问邻居。

注意:我们可能有未连接的图形。未连接图是在任何两个节点之间没有路径的图。

在此示例中,将访问节点0、1和2,输出将显示这些节点,并完全忽略节点3和4。

如果我们调用depthFirstSearch(4),将会发生类似的情况,仅这次会访问4和3,而不会访问0、1和2。解决此问题的方法是,只要有任何未访问的节点,就继续调用DFS。

这可以通过几种方法来完成,但是我们可以对我们的Graph类进行另一个小的修改以处理此问题。我们将添加一个新depthFirstSearchModified(Node node)方法:

public void depthFirstSearchModified(Node node) {
depthFirstSearch(node);
for (Node n : adjacencyMap.keySet()) {
if (!n.isVisited()) {
depthFirstSearch(n);
}
}
}
public void depthFirstSearch(Node node) {
node.visit();
System.out.print(node.name + " ");
LinkedList allNeighbors = adjacencyMap.get(node);
if (allNeighbors == null)
return;
for (Node neighbor : allNeighbors) {
if (!neighbor.isVisited())
depthFirstSearch(neighbor);
}
}
public class GraphShow {
public static void main(String[] args) {
Graph graph = new Graph(false);
Node a = new Node(0, "0");
Node b = new Node(1, "1");
Node c = new Node(2, "2");
Node d = new Node(3, "3");
Node e = new Node(4, "4");
graph.addEdge(a,b);
graph.addEdge(a,c);
graph.addEdge(c,b);
graph.addEdge(e,d);
System.out.println("If we were to use our previous DFS method, we would get an incomplete traversal");
graph.depthFirstSearch(b);
graph.resetNodesVisited(); // All nodes are marked as visited because of
// the previous DFS algorithm so we need to
// mark them all as not visited
System.out.println();
System.out.println("Using the modified method visits all nodes of the graph, even if it's unconnected");
graph.depthFirstSearchModified(b);
}
}

这给了我们输出:

If we were to use our previous DFS method, we would get an incomplete traversal

1 0 2

Using the modified method visits all nodes of the graph, even if it's unconnected

1 0 2 4 3

让我们在另一个示例上运行我们的算法:

public class GraphShow {
public static void main(String[] args) {
Graph graph = new Graph(true);
Node zero = new Node(0, "0");
Node one = new Node(1, "1");
Node two = new Node(2, "2");
Node three = new Node(3, "3");
Node four = new Node(4, "4");
Node five = new Node(5, "5");
Node six = new Node(6, "6");
Node seven = new Node(7, "7");
Node eight = new Node(8, "8");
graph.addEdge(one,zero);
graph.addEdge(three,one);
graph.addEdge(two,seven);
graph.addEdge(two,four);
graph.addEdge(five,two);
graph.addEdge(five,zero);
graph.addEdge(six,five);
graph.addEdge(six,three);
graph.addEdge(six,eight);
graph.addEdge(seven,five);
graph.addEdge(seven,six);
graph.addEdge(seven,eight);
graph.depthFirstSearch(seven);
}
}

这给了我们输出:

7 5 2 4 0 6 3 1 8

订购邻居

我们可能要添加的另一项“有趣”的事情是每个节点列出邻居的顺序。我们可以通过使用堆数据结构(PriorityQueue在Java中)代替LinkedListfor邻居来实现此目的,并compareTo()在Node类中实现一个方法,以便Java知道如何对对象进行排序:

public class Node implements Comparable {
// Same code as before...
public int compareTo(Node node) {
return this.n - node.n;
}
}
class Graph {
// Replace all occurrences of LinkedList with PriorityQueue
}
public class GraphShow {
public static void main(String[] args) {
GraphAdjacencyList graph = new GraphAdjacencyList(true);
Node a = new Node(0, "0");
Node b = new Node(1, "1");
Node c = new Node(2, "2");
Node d = new Node(3, "3");
Node e = new Node(4, "4");
graph.addEdge(a,e);
graph.addEdge(a,d);
graph.addEdge(a,b);
graph.addEdge(a,c);
System.out.println("When using a PriorityQueue, it doesn't matter in which order we add neighbors, they will always be sorted");
graph.printEdges();
System.out.println();
graph.depthFirstSearchModified(a);
graph.resetNodesVisited();
}
}
When using a PriorityQueue, it doesn't matter in which order we add neighbors, they will always be sorted
The 0 has an edge towards: 1 2 3 4
0 1 2 3 4

如果我们不使用a PriorityQueue,则DFS输出将为0,4,3,1,2。

结论

图形是存储某些类型的数据的便捷方法。该概念是从数学移植而来的,适合于计算机科学的需求。

由于许多事物可以用图形表示,因此图形遍历已成为一项常见的任务,尤其是在数据科学和机器学习中。

深度优先搜索(DFS)是为数不多的图形遍历算法之一,它沿分支尽可能远地搜索,然后回溯以在下一个分支中尽可能地搜索。