En esta práctica se realizará la implementación de un grafo dirigido y un grafo no dirigido con la ayuda de una clase Padre Grafo.
Temas utilizados:
- Herencia
- Clases Abstractas
- Clases Anidadas
- Interfaces
- Polimorfismo
Dentro de nuestro pom, el siguiente código permitirá a maven compilar y ejecutar nuestro proyecto.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<mainClass>edu.upvictoria.fpoo.App</mainClass>
</configuration>
</plugin>
</plugins>
</build>
mvn clean && mvn validate && mvn compile
mvn exec:java
Crearemos una clase Grafo como plantilla para representar a los grafos dirigidos como los no dirigidos. Esta sería una estructura basica y ya que los grafos comparten vbaaaCrearemos una clase Grafo como plantilla para representar a los grafos dirigidos como los no dirigidos.
Podemos decir que los grafos comparten las siguientes características:
- Imprimir el grafo
- Añadir un vértice
- Añadir una arista
No importa si es dirigido o no dirigido, los dos grafos comparten esos comportamientos así que es posible crear una interfaz a partir de esto.
public interface GraphUtils<T> {
void printGraph();
void addVertex(T vertex);
void addEdge(T sourceVertex, T destinationVertex);
}
Una vez tenemos la interfaz podemos proceder con la creación de la clase padre Grafo:
public abstract class Graph<T> implements GraphUtils<T> {
protected Map<T, List<Nodo<T>>> vertices;
public Graph() {
this.vertices = new HashMap<>();
}
public static class Nodo<T> {
private T data;
public Nodo(T data) {
this.data = data;
}
public T getData() {
return data;
}
@Override
public String toString() {
return data.toString();
}
}
public boolean hasVertex(T vertex) {
return vertices.containsKey(vertex);
}
public void addVertex(T vertex) {
if (!hasVertex(vertex)) {
vertices.put(vertex, new ArrayList<>());
}
}
}
La clase Nodo nos brinda más flexibilidad para crear nuestros propios contenedores. Aunque
también se podría utilizar un tipo de dato más simple como List<T>
.
El método addVertex()
permite añadir vértices al grafo sin repetición.
A través de la herencia, podemos crear una clase que extienda la funcionalidad de nuestra clase Grafo:
public class UndirectedGraph<T> extends Graph<T> {
public UndirectedGraph() {
super();
}
}
Como se puede notar, no hace falta volver a implementar la interfaz GraphUtils
debido a que la clase padre ya la esta implementando.
Los grafos no dirigidos son bidireccionales así que para agregar una arista se tiene que agregar tanto al vértice de destino como al vecino:
@Override
public void addEdge(T sourceVertex, T destinationVertex) {
if (hasVertex(sourceVertex) && hasVertex(destinationVertex)) {
List<Nodo<T>> sourceNeighbors = vertices.get(sourceVertex);
List<Nodo<T>> destinationNeighbors = vertices.get(destinationVertex);
sourceNeighbors.add(new Nodo<>(destinationVertex));
destinationNeighbors.add(new Nodo<>(sourceVertex));
}
}
Por último se añadirá el método para imprimir el grafo no dirigido:
@Override
public void printGraph() {
for (Map.Entry<T, List<Nodo<T>>> entry : vertices.entrySet()) {
T vertex = entry.getKey();
List<Nodo<T>> neighbors = entry.getValue();
System.out.print(vertex + " -> ");
for (Nodo<T> neighbor : neighbors) {
System.out.print(neighbor.getData() + " ");
}
System.out.println();
}
}
Se recorre el grafo, se obtiene el nodo actual y se recorre la lista de sus vecinos.
Estructura básica para la clase:
public class DirectedGraph<T> extends Graph<T> {
public DirectedGraph() {
super();
}
}
Al contrario que el grafo no dirigido, aquí solo se agrega una vez el vértice cuando se añade una nueva arista:
@Override
public void addEdge(T sourceVertex, T destinationVertex) {
if (vertices.containsKey(sourceVertex) && vertices.containsKey(destinationVertex)) {
List<Nodo<T>> neighbors = vertices.get(sourceVertex);
neighbors.add(new Nodo<>(destinationVertex));
}
}
A diferencia del printGraph()
del grafo no dirigido, radica en como se están representando las
aristas así que para imprimirlo se mostrarán las conecciones de manera unidireccional:
@Override
public void printGraph() {
for (Map.Entry<T, List<Nodo<T>>> entry : vertices.entrySet()) {
T vertex = entry.getKey();
List<Nodo<T>> neighbors = entry.getValue();
System.out.print(vertex + " -> ");
for (Nodo<T> neighbor : neighbors) {
System.out.print(neighbor.getData() + " ");
}
System.out.println();
}
}
En esta clase simplemente tenemos un main donde se crean instancias de las clases de los grafos:
public class App
{
public static void main( String[] args ) {
DirectedGraph<Integer> directedGraph = new DirectedGraph<>();
directedGraph.addVertex(1);
directedGraph.addVertex(2);
directedGraph.addVertex(3);
directedGraph.addEdge(1, 2);
directedGraph.addEdge(2, 3);
System.out.println("Grafo Dirigido:");
directedGraph.printGraph();
Graph<String> undirectedGraph = new UndirectedGraph<>();
undirectedGraph.addVertex("A");
undirectedGraph.addVertex("B");
undirectedGraph.addVertex("C");
undirectedGraph.addEdge("A", "B");
undirectedGraph.addEdge("B", "C");
System.out.println("Grafo No Dirigido:");
undirectedGraph.printGraph();
}
}
Salida:
Grafo Dirigido:
1 -> 2
2 -> 3
3 ->
Grafo No Dirigido:
A -> B
B -> A C
C -> B