diff --git a/README.md b/README.md index cab5dbd..be5f812 100644 --- a/README.md +++ b/README.md @@ -22,5 +22,5 @@ A Python library for data structures. This library provides more than 20 advance - [ ] Skip List - [x] Trie - [x] Max Heap -- [ ] Graph (Adjacency Matrix) -- [ ] Graph (Edge List) +- [x] Graph (Adjacency Matrix) +- [ ] Graph (Adjacency List) diff --git a/pystrukts/graph/__init__.py b/pystrukts/graph/__init__.py new file mode 100644 index 0000000..d801f20 --- /dev/null +++ b/pystrukts/graph/__init__.py @@ -0,0 +1,5 @@ +# pylint: skip-file + +from .graph import Graph +# from .adjacency_list import AdjacencyList +from .adjacency_matrix import AdjacencyMatrix \ No newline at end of file diff --git a/pystrukts/graph/adjacency_matrix.py b/pystrukts/graph/adjacency_matrix.py new file mode 100644 index 0000000..10f0e42 --- /dev/null +++ b/pystrukts/graph/adjacency_matrix.py @@ -0,0 +1,248 @@ +''' +Adjacency matrix representation of a graph. + +This module provides the AdjacencyMatrix class, which represents a graph using an adjacency matrix. +''' + +from warnings import warn +from pystrukts.graph import Graph + +class AdjacencyMatrix(Graph): + ''' + Graph representation using adjacency matrix. + + Attributes: + num_vertices (int): The number of vertices in the graph. + directed (bool): True if the graph is directed, False otherwise. + matrix (list): The adjacency matrix of the graph. + no_edge_value (float): The value used to represent the absence of an edge. + + Methods: + copy() -> AdjacencyMatrix: Create a copy of the graph. + _copy_matrix(matrix: list, directed: bool) -> list: Copy the adjacency matrix. + _raise_error_if_invalid_vertex(x: int) -> None: Raise an error if the vertex does not exist. + has_edge(x: int, y: int) -> bool: Check if there is an edge between two vertices. + neighbors(x: int) -> list: Get the neighbors of a vertex. + add_edge(x: int, y: int, w: float = 1.0) -> None: Add an edge between two vertices. + If the edge already exists, the weight is updated. + remove_edge(x: int, y: int) -> None: Remove an edge between two vertices. + add_vertex() -> None: Add an isolated vertex to the graph. + remove_vertex(x: int) -> None: Remove a vertex from the graph. + get_edge_weight(x: int, y: int) -> float: Get the weight of an edge. + set_edge_weight(x: int, y: int, w: float) -> None: Set the weight of an edge. + ''' + + def __init__(self, num_vertices: int = None, directed: bool = False, + matrix: list = None, no_edge_value: float = float('inf')) -> None: + ''' + Initialize the adjacency matrix. + + Args: + num_vertices (int): The number of vertices in the graph. + directed (bool): True if the graph is directed, False otherwise. + matrix (list): The adjacency matrix of the graph. + no_edge_value (float): The value used to represent the absence of an edge. + + Raises: + ValueError: No adjacency matrix or number of vertices provided. + ''' + + if not matrix and not num_vertices: + raise ValueError('You must specify either the number of vertices \ + or the adjacency matrix.') + + if matrix and num_vertices: + warn('The number of vertices will be ignored, as an adjacency matrix was provided.') + + self.num_vertices = len(matrix) if matrix is not None else num_vertices + self.directed = directed + self.no_edge_value = no_edge_value + + if matrix is not None: + for row in matrix: + if len(row) != len(matrix): + raise ValueError('The number of columns in the matrix must be \ + equal to the number of vertices.') + + self.matrix = self._copy_matrix(matrix, directed) + elif num_vertices < 0: + raise ValueError('The number of vertices must be a non-negative integer.') + else: + self.matrix = [[no_edge_value for _ in range(num_vertices)] + for _ in range(num_vertices)] + + def copy(self) -> 'AdjacencyMatrix': + ''' + Create a copy of the graph. + + Returns: + out (AdjacencyMatrix): The copy of the graph. + ''' + + return AdjacencyMatrix(directed = self.directed, matrix = self.matrix) + + def _copy_matrix(self, matrix: list, directed: bool) -> list: + ''' + Copy the adjacency matrix with floating point values. + If the graph is undirected, the matrix is made symmetric by copying the upper triangle. + + + Args: + matrix (list): The matrix to be copied. + directed (bool): True if the graph is directed, False otherwise. + + Returns: + out (list): The symmetric matrix. + ''' + + if directed: + return [[matrix[i][j] if i <= j else matrix[j][i] + for j in range(self.num_vertices)] for i in range(self.num_vertices)] + return [[float(matrix[i][j]) for j in range(self.num_vertices)] + for i in range(self.num_vertices)] + + def _raise_error_if_invalid_vertex(self, x: int) -> None: + ''' + Raise an error if the vertex does not exist. + + Args: + x (int): The vertex. + + Raises: + ValueError: If the vertex does not exist. + ''' + if x >= self.num_vertices or x < 0: + raise ValueError(f'The vertex {x} does not exist.') + + def has_edge(self, x: int, y: int) -> bool: + ''' + Check if there is an edge between two vertices. + If the vertices are the same, the function checks if the vertex has a self-loop. + + Args: + x (int): The source vertex. + y (int): The destination vertex. + + Returns: + out (bool): True if there is an edge between the vertices, False otherwise. + + Raises: + ValueError: If x or y are not valid vertices. + ''' + + self._raise_error_if_invalid_vertex(x) + self._raise_error_if_invalid_vertex(y) + + return self.matrix[x][y] != self.no_edge_value + + def neighbors(self, x: int) -> list: + ''' + Get the neighbors of a vertex. + If x has a self-loop, the output contains the vertex itself. + + Args: + x (int): The vertex. + + Returns: + out (list): The list of neighbors of the vertex. + + Raises: + ValueError: If x is not a valid vertex. + ''' + + self._raise_error_if_invalid_vertex(x) + + return [i for i in range(self.num_vertices) if self.matrix[x][i] != self.no_edge_value] + + def add_edge(self, x: int, y: int, w: float = 1.0) -> None: + ''' + Add an edge between two vertices. If the edge already exists, the weight is updated. + + Args: + x (int): The source vertex. + y (int): The destination vertex. + w (float): The weight of the edge. + + Raises: + ValueError: If x or y are not valid vertices. + ''' + + self._raise_error_if_invalid_vertex(x) + self._raise_error_if_invalid_vertex(y) + + self.matrix[x][y] = w + + if self.directed: + self.matrix[y][x] = w + + def remove_edge(self, x: int, y: int) -> None: + ''' + Remove an edge between two vertices. + + Args: + x (int): The source vertex. + y (int): The destination vertex. + + Raises: + ValueError: If x or y are not valid vertices. + ''' + + self._raise_error_if_invalid_vertex(x) + self._raise_error_if_invalid_vertex(y) + + self.matrix[x][y] = self.no_edge_value + + if self.directed: + self.matrix[y][x] = self.no_edge_value + + def add_vertex(self) -> None: + ''' + Add an isolated vertex to the graph. + ''' + + self.num_vertices += 1 + + for row in self.matrix: + row.append(self.no_edge_value) + + self.matrix.append([self.no_edge_value for _ in range(self.num_vertices)]) + + def remove_vertex(self, x: int) -> None: + ''' + Remove a vertex from the graph. + + Args: + x (int): The vertex to be removed. + + Raises: + ValueError: If the vertex does not exist. + ''' + + self._raise_error_if_invalid_vertex(x) + + self.num_vertices -= 1 + + for row in self.matrix: + del row[x] + + del self.matrix[x] + + def get_edge_weight(self, x: int, y: int) -> float: + ''' + Get the weight of an edge. + + Args: + x (int): The source vertex. + y (int): The destination vertex. + + Returns: + out (float): The weight of the edge. + + Raises: + ValueError: If x or y are not valid vertices. + ''' + + self._raise_error_if_invalid_vertex(x) + self._raise_error_if_invalid_vertex(y) + + return self.matrix[x][y] diff --git a/pystrukts/graph/graph.py b/pystrukts/graph/graph.py new file mode 100644 index 0000000..5b6218e --- /dev/null +++ b/pystrukts/graph/graph.py @@ -0,0 +1,104 @@ +''' +Module for the Graph abstract class. +''' + +from abc import ABCMeta, abstractmethod + +class Graph(metaclass=ABCMeta): + ''' + Abstract class for graph representations. + + Methods: + copy() -> Graph: Create a copy of the graph. + has_edge(x: int, y: int) -> bool: Check if there is an edge between two vertices. + neighbors(x: int) -> list: Get the neighbors of a vertex. + add_edge(x: int, y: int, w: float = 1.0) -> None: Add an edge between two vertices. + If the edge already exists, the weight is updated. + remove_edge(x: int, y: int) -> None: Remove an edge between two vertices. + add_vertex() -> None: Add an isolated vertex to the graph. + remove_vertex(x: int) -> None: Remove a vertex from the graph. + get_edge_weight(x: int, y: int) -> float: Get the weight of an edge. + ''' + + @abstractmethod + def copy(self): + ''' + Create a copy of the graph. + + Returns: + Graph: A copy of the graph. + ''' + + @abstractmethod + def has_edge(self, x: int, y: int) -> bool: + ''' + Check if there is an edge between two vertices. + + Args: + x (int): The source vertex. + y (int): The destination vertex. + + Returns: + out (bool): True if there is an edge between the vertices, False otherwise. + ''' + + @abstractmethod + def neighbors(self, x: int) -> list: + ''' + Get the neighbors of a vertex. + + Args: + x (int): The vertex. + + Returns: + out (list): The neighbors of the vertex. + ''' + + @abstractmethod + def add_edge(self, x: int, y: int, w: float = 1.0): + ''' + Add an edge between two vertices. + + Args: + x (int): The source vertex. + y (int): The destination vertex. + w (float): The weight of the edge. + ''' + + @abstractmethod + def remove_edge(self, x: int, y: int): + ''' + Remove an edge between two vertices. + + Args: + x (int): The source vertex. + y (int): The destination vertex. + ''' + + @abstractmethod + def add_vertex(self): + ''' + Add an isolated vertex to the graph. + ''' + + @abstractmethod + def remove_vertex(self, x: int): + ''' + Remove a vertex from the graph. + + Args: + x (int): The vertex. + ''' + + @abstractmethod + def get_edge_weight(self, x: int, y: int) -> float: + ''' + Get the weight of an edge. + + Args: + x (int): The source vertex. + y (int): The destination vertex. + + Returns: + out (float): The weight of the edge. + ''' diff --git a/tests/test_adjacency_matrix.py b/tests/test_adjacency_matrix.py new file mode 100644 index 0000000..2b168de --- /dev/null +++ b/tests/test_adjacency_matrix.py @@ -0,0 +1,231 @@ +import pytest +from pystrukts.graph import AdjacencyMatrix + +def test_initilization(): + matrix = [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + graph = AdjacencyMatrix(matrix=matrix) + assert graph.num_vertices == 3 + assert graph.directed == False + assert graph.matrix == [[0., 1., 2.], [3., 4., 5.], [6., 7., 8.]] + + graph2 = AdjacencyMatrix(matrix=matrix, directed=True) + assert graph2.num_vertices == 3 + assert graph2.directed == True + assert graph2.matrix == [[0., 1., 2.], [1., 4., 5.], [2., 5., 8.]] # _copy_upper + + graph3 = AdjacencyMatrix(3) + assert graph3.num_vertices == 3 + assert graph3.directed == False + assert graph3.matrix == [[float('inf') for _ in range(3)] for _ in range(3)] + + graph4 = AdjacencyMatrix(3, no_edge_value=0) + assert graph4.num_vertices == 3 + assert graph4.directed == False + assert graph4.matrix == [[0 for _ in range(3)] for _ in range(3)] + + # num_vertices ignored + with pytest.warns(UserWarning): + graph5 = AdjacencyMatrix(2, matrix = matrix) + assert graph5.num_vertices == 3 + assert graph5.directed == False + assert graph5.matrix == matrix + +def test_initialization_error(): + with pytest.raises(ValueError): + graph = AdjacencyMatrix(-1) + + with pytest.raises(ValueError): + matrix = [[0, 1, 2], [3], [6, 7, 8]] + graph = AdjacencyMatrix(matrix = matrix) + + with pytest.raises(ValueError): + graph = AdjacencyMatrix() + +def test_copy(): + matrix = [[0, 1, 2], [3, 4, 5], [6, 7, 8]] + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + graph2 = graph.copy() + assert graph2.num_vertices == 3 + assert graph2.directed == False + assert graph2.matrix == matrix + +def test_has_edge(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + assert graph.has_edge(0, 0) == False + assert graph.has_edge(0, 1) == True + assert graph.has_edge(0, 2) == False + assert graph.has_edge(1, 0) == False + assert graph.has_edge(1, 1) == False + assert graph.has_edge(1, 2) == False + assert graph.has_edge(2, 0) == True + assert graph.has_edge(2, 1) == False + assert graph.has_edge(2, 2) == True + +def test_has_edge_error(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + graph = AdjacencyMatrix(matrix=matrix) + + with pytest.raises(ValueError): + graph.has_edge(0, 3) + + with pytest.raises(ValueError): + graph.has_edge(-1, 0) + +def test_neighbors(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + assert graph.neighbors(0) == [1] + assert graph.neighbors(1) == [] + assert graph.neighbors(2) == [0, 2] + +def test_neighbors_error(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + + with pytest.raises(ValueError): + graph.neighbors(3) + + with pytest.raises(ValueError): + graph.neighbors(-1) + +def test_add_edge(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + + assert graph.has_edge(0, 2) == False + graph.add_edge(0, 2) + assert graph.has_edge(0, 2) == True + assert graph.matrix == [[0., 1., 1.], [0., 0., 0.], [1., 0., 1.]] + + assert graph.has_edge(1, 2) == False + graph.add_edge(1, 2, 25) + assert graph.has_edge(1, 2) == True + assert graph.matrix == [[0., 1., 1.], [0., 0., 25.], [1., 0., 1.]] + +def test_add_edge_directed(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + + graph = AdjacencyMatrix(3, directed=True, no_edge_value=0) + + assert graph.has_edge(0, 2) == False + assert graph.has_edge(2, 0) == False + graph.add_edge(0, 2) + assert graph.has_edge(0, 2) == True + assert graph.has_edge(2, 0) == True + assert graph.get_edge_weight(0, 2) == 1 + assert graph.get_edge_weight(2, 0) == 1 + + assert graph.has_edge(1, 2) == False + assert graph.has_edge(2, 1) == False + graph.add_edge(1, 2, 25) + assert graph.has_edge(1, 2) == True + assert graph.has_edge(2, 1) == True + assert graph.get_edge_weight(1, 2) == 25 + assert graph.get_edge_weight(2, 1) == 25 + +def test_add_edge_error(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + + with pytest.raises(ValueError): + graph.add_edge(0, 3) + + with pytest.raises(ValueError): + graph.add_edge(-1, 0) + +def test_remove_edge(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + + assert graph.has_edge(0, 1) == True + graph.remove_edge(0, 1) + assert graph.has_edge(0, 1) == False + assert graph.matrix == [[0., 0., 0.], [0., 0., 0.], [1., 0., 1.]] + + assert graph.has_edge(2, 0) == True + graph.remove_edge(2, 0) + assert graph.has_edge(2, 0) == False + assert graph.matrix == [[0., 0., 0.], [0., 0., 0.], [0., 0., 1.]] + +def test_remove_edge_directed(): + matrix = [[1, 1, 0], [1, 1, 0], [0, 0, 0]] + graph = AdjacencyMatrix(matrix=matrix, directed=True, no_edge_value=0) + + assert graph.has_edge(0, 1) == True + assert graph.has_edge(1, 0) == True + graph.remove_edge(0, 1) + assert graph.has_edge(0, 1) == False + assert graph.has_edge(1, 0) == False + assert graph.matrix == [[1., 0., 0.], [0., 1., 0.], [0., 0., 0.]] + + graph.remove_edge(0, 0) + assert graph.matrix == [[0., 0., 0.], [0., 1., 0.], [0., 0., 0.]] + +def test_remove_edge_error(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + + with pytest.raises(ValueError): + graph.remove_edge(0, 3) + + with pytest.raises(ValueError): + graph.remove_edge(-1, 0) + +def test_add_vertex(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + assert graph.num_vertices == 3 + assert graph.matrix == [[0., 1., 0.], [0., 0., 0.], [1., 0., 1.]] + + graph.add_vertex() + assert graph.num_vertices == 4 + assert graph.matrix == [[0., 1., 0., 0.], [0., 0., 0., 0.], [1., 0., 1., 0.], [0., 0., 0., 0.]] + +def test_remove_vertex(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + assert graph.num_vertices == 3 + assert graph.matrix == [[0., 1., 0.], [0., 0., 0.], [1., 0., 1.]] + + graph.remove_vertex(1) + assert graph.num_vertices == 2 + assert graph.matrix == [[0., 0.], [1., 1.]] + + graph.remove_vertex(1) + assert graph.num_vertices == 1 + assert graph.matrix == [[0.]] + +def test_remove_vertex_error(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + + with pytest.raises(ValueError): + graph.remove_vertex(3) + + with pytest.raises(ValueError): + graph.remove_vertex(-1) + +def test_get_edge_weight(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + assert graph.get_edge_weight(0, 1) == 1 + assert graph.get_edge_weight(1, 0) == 0 + assert graph.get_edge_weight(2, 0) == 1 + +def test_get_edge_weight_error(): + matrix = [[0, 1, 0], [0, 0, 0], [1, 0, 1]] + graph = AdjacencyMatrix(matrix=matrix, no_edge_value=0) + + with pytest.raises(ValueError): + graph.get_edge_weight(0, 3) + + with pytest.raises(ValueError): + graph.get_edge_weight(-1, 0)