Автор оригинала: Olivera Popović.
Вступление
Графики-это удобный способ хранения определенных типов данных. Концепция была перенесена из математики и приспособлена для нужд информатики.
В связи с тем, что многие вещи могут быть представлены в виде графиков, обход графиков стал обычной задачей, особенно используемой в науке о данных и машинном обучении.
- Графики на Java
- Представление графиков в коде
- Поиск по глубине (DFS)
- Поиск по ширине (BFS)
- Алгоритм Дейкстры
Поиск по глубине
Поиск по глубине (DFS) выполняет поиск как можно дальше вдоль ветви, а затем возвращается к поиску как можно дальше в следующей ветви. Это означает, что на следующем графике он начинается с первого соседа и продолжается вниз по линии, насколько это возможно:
Как только он достигает последнего узла в этой ветви (1), он возвращается к первому узлу, где он столкнулся с возможностью изменить курс (5), и посещает всю эту ветвь, которая в нашем случае является узлом (2).
Затем он снова возвращается к узлу (5), и поскольку он уже посетил узлы (1) и (2), он возвращается к (3) и перенаправляется в следующую ветвь (8).
Реализация
Поскольку мы знаем, как представлять графики в коде с помощью списков смежности и матриц, давайте составим график и пройдемся по нему с помощью DFS. Графики, с которыми мы будем работать, достаточно просты, поэтому не имеет значения, какую реализацию мы выберем.
Хотя для реальных проектов в большинстве случаев списки смежности будут лучшим выбором, поэтому мы представим график в виде списка смежности.
Мы хотим посетить все наши узлы один раз, как показано на анимации выше, они становятся красными после посещения, поэтому мы их больше не посещаем. Чтобы сделать это в коде, мы введем флаг посещено
:
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; } }
Теперь давайте определим График
:
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<>(); } // ... }
Теперь давайте добавим метод added()
. Мы будем использовать два метода: вспомогательный метод и фактический метод.
В вспомогательном методе мы также проверим наличие возможных повторяющихся ребер. Прежде чем добавлять границу между A
и B
, мы сначала удалим ее и только затем добавим. Если ребро уже существовало, это не позволяет нам добавить дубликат ребра. Если там еще не было ребра, то у нас все еще есть только одно ребро между двумя узлами.
Если ребро не существовало, удаление несуществующего ребра приведет к исключению NullPointerException
поэтому мы вводим временную копию списка:
public void addEdgeHelper(Node a, Node b) { LinkedListtmp = 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); } }
Наконец, у нас будут края печати()
, есть края()
и посещенные узлы сброса()
вспомогательные методы, которые довольно просты:
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(); } }
Мы также добавим метод первого поиска по глубине(узел узла)
в наш График
класс, который выполняет следующее:
- Если
узел.посетил
, просто верните - Если он еще не был посещен, выполните следующие действия:
- Найдите первого незваного соседа
новый узел
изузла
и вызовитепервый поиск по глубине(новый узел)
- Повторите процесс для всех незваных соседей
- Найдите первого незваного соседа
Давайте проиллюстрируем это примером:
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
- Все узлы в начале не посещены (
узел.посещен
) - Вызовите
.первый поиск по глубине()
с произвольным узлом в качестве начального узла, скажемпервый поиск по глубине(B)
- отметьте B как посещенный
- Есть ли у Б какие-нибудь незваные соседи? Да -> первым не посещенным узлом является D, поэтому вызовите
первый поиск по глубине(D)
- марк Дас посетил
- Есть ли у Д. какие-нибудь незваные соседи? Нет -> (B уже был посещен) возврат
- Есть ли у Б какие-нибудь незваные соседи? Да -> первым не посещенным узлом является C, поэтому вызовите
первый поиск по глубине(C)
- отметьте C как посещенный
- Есть ли у С какие-нибудь незваные соседи? Да -> первым не посещенным узлом является A, поэтому вызовите
первый поиск по глубине(A)
1. отметьте A как посещенный 2. Есть ли у A незваные соседи? Нет. -> возврат - Есть ли у С какие-нибудь незваные соседи? Нет -> возврат
- Есть ли у Б какие-нибудь незваные соседи? Нет -> возврат
Вызов DFS на нашем графике дал бы нам обход B,D,C,A (порядок посещения). Когда алгоритм написан таким образом, его легко перевести в код:
Git Essentials
Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!
public void depthFirstSearch(Node node) { node.visit(); System.out.print(node.name + " "); LinkedListallNeighbors = 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.
Аналогичная вещь произошла бы , если бы мы вызвали Первый поиск по глубине(4)
, только на этот раз 4 и 3 были бы посещены, а 0, 1 и 2-нет. Решение этой проблемы состоит в том, чтобы продолжать вызывать DFS до тех пор, пока есть какие-либо не посещенные узлы.
Это можно сделать несколькими способами, но мы можем внести еще одну небольшую модификацию в наш класс Graph
, чтобы решить эту проблему. Мы добавим новый Первый измененный поиск по глубине(узел узла)
метод:
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 + " "); LinkedListallNeighbors = 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
Заказ Соседей
Еще одна “забавная” вещь, которую мы, возможно, захотим добавить, – это некоторый порядок, в котором соседи перечислены для каждого узла. Мы можем достичь этого, используя структуру данных кучи ( Очередь приоритетов
в Java) вместо Связанного списка
для соседей и реализуя метод compareTo()
в нашем Узле
классе, чтобы 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
Если бы мы не использовали Приоритетную очередь
, вывод DFS был бы 0,4,3,1,2 .
Вывод
Графики-это удобный способ хранения определенных типов данных. Концепция была перенесена из математики и приспособлена для нужд информатики.
В связи с тем, что многие вещи могут быть представлены в виде графиков, обход графиков стал обычной задачей, особенно используемой в науке о данных и машинном обучении.
Поиск по глубине (DFS)-один из немногих алгоритмов обхода графов, который выполняет поиск как можно дальше по ветви, а затем возвращается к поиску как можно дальше в следующей ветви.