dsa.graph

Module containing graph classes.

  1""" Module containing graph classes. """
  2
  3from dsa.queue import Queue
  4
  5class AdjacencyMatrixGraph:
  6    """ 
  7    An unweighted adjacency matrix graph implementation.
  8    
  9    This class allows either directed or undirected representation of a graph.
 10    Vertex labels are string types.
 11    """
 12    def __init__(self, labels: list[str]):
 13        """ 
 14        Initialize the graph with a list of vertex labels.
 15
 16        Args:
 17            labels (list[str]): List of labels for each vertex.
 18        """
 19        self.labels = labels
 20        self.label_index = { label: index for index, label  in enumerate(labels) }
 21
 22        node_count = len(self.labels)
 23        self.array = [[None for i in range(node_count)] for j in range(node_count)]
 24
 25    def add_edge(self, start_label: str, end_label: str, directed=False):
 26        """ 
 27        Add an edge in the graph.
 28        
 29        Args:
 30            start_label (str): Starting vertex label.
 31            end_label (str): Ending vertex label.
 32            directed (bool): Whether the edge is directed.
 33        """
 34        a = self.label_index[start_label]
 35        b = self.label_index[end_label]
 36        self.array[a][b] = True
 37        if not directed:
 38            self.array[b][a] = True
 39
 40    def add_vertex(self, label: str):
 41        """ 
 42        Add a vertex to the graph.
 43        
 44        Args:
 45            label (str): The vertex label to add.
 46        """
 47        self.labels.append(label)
 48        self.label_index[label] = len(self.labels) - 1
 49        for row in self.array:
 50            row.append(None)
 51        self.array.append([None for i in range(len(self.labels))])
 52
 53    def delete_vertex(self, label: str):
 54        """ 
 55        Delete a vertex from the graph.
 56        
 57        Args:
 58            label (str): The vertex label to delete.
 59        """
 60        index = self.label_index[label]
 61        self.labels.pop(index)
 62        self.label_index = { label: index for index, label in enumerate(self.labels) }
 63        self.array.pop(index)
 64        for row in self.array:
 65            row.pop(index)
 66
 67    def delete_edge(self, start_label: str, end_label: str, directed=False):
 68        """ 
 69        Delete an edge in the graph.
 70        
 71        Args:
 72            start_label (str): Starting vertex label.
 73            end_label (str): Ending vertex label.
 74            directed (bool): Whether the edge is directed.
 75        """
 76        a = self.label_index[start_label]
 77        b = self.label_index[end_label]
 78        if self.array[a][b] is None:
 79            raise KeyError(f"Edge {start_label} to {end_label} does not exist")
 80
 81        self.array[a][b] = None
 82        if not directed:
 83            if self.array[b][a] is None:
 84                raise KeyError(f"Edge {end_label} to {start_label} does not exist")
 85            self.array[b][a] = None
 86
 87    def dfs_traverse(self, start_label: str):
 88        """ 
 89        Perform depth first traversal in an adjacency matrix
 90 
 91        Args:
 92            start_label (str): Starting vertex label.
 93        
 94        Returns:
 95            Array with depth first order traversal.
 96        """
 97        return self._df_rec_traverse(start_label, set(), [])
 98        
 99    def _df_rec_traverse(self, start_label: str, visited, dfs):
100        """ 
101        Helper method for depth first recursive traversal
102        """
103        start_index = self.label_index[start_label]
104        visited.add(start_label)
105        dfs.append(self.labels[start_index])
106
107        for adj_index, is_connected in enumerate(self.array[start_index]):
108            adj_label = self.labels[adj_index]
109            if is_connected and adj_label not in visited:
110                self._df_rec_traverse(adj_label, visited, dfs)
111
112        return dfs
113
114    def bfs_traverse(self, start_label: str):
115        """ 
116        Perform breadth first traversal in an adjacency matrix.
117 
118        Args:
119            start_label (str): Starting vertex label.
120        
121        Returns:
122            Array with breadth first order traversal.
123        """
124        bfs = []
125        q = Queue()
126        visited = set()
127
128        start_index = self.label_index[start_label]
129        q.enqueue(start_index)
130        bfs.append(self.labels[start_index])
131        while not q.is_empty():
132            current = q.dequeue()
133            for i in range(len(self.array)):
134                if start_index != i and self.array[current][i] and (i not in visited):
135                    visited.add(i)
136                    q.enqueue(i)
137                    bfs.append(self.labels[i])
138        return bfs
139    
140    def vertices(self) -> list:
141        """
142        Return a list of vertex labels of the graph
143        """
144        return self.labels
145    
146    def edges(self) -> list:
147        """ 
148        Return a list of edges in the graph. Each edge is represented by a tuple (start, end)
149        """
150        edges = []
151        vertex_count = len(self.labels)
152        for i in range(vertex_count):
153            for j in range(vertex_count):
154                if self.array[i][j] and i != j:  
155                    edges.append((self.labels[i], self.labels[j]))
156    
157        return edges
158
159    def undirected_edges(self) -> list:
160        """ 
161        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end)
162        """
163        edges = []
164        vertex_count = len(self.labels)
165        for i in range(vertex_count):
166            for j in range(vertex_count):
167                if self.array[i][j] and i != j and (self.labels[j], self.labels[i]) not in edges:  
168                    edges.append((self.labels[i], self.labels[j]))
169    
170        return edges
171
172    def is_edge(self, start_label: str, end_label: str) -> bool:
173        """ 
174        Return boolean if an edge exists.
175
176        Args:
177            start_label (str): starting vertex label
178            end_label (str): starting vertex label
179        
180        Returns:0
181            A boolean of whether there is an edge from start to end.
182        """
183        start_index = self.label_index[start_label]
184        end_index = self.label_index[end_label]
185
186        return self.array[start_index][end_index] is not None
187
188    def __getitem__(self, vertex: str) -> list:
189        """ 
190        Args:
191            vertex (str): The vertex label.
192        Returns:
193            A list of adjacent vertex labels.
194        """
195        index = self.label_index[vertex]
196        return [self.labels[i] for i in range(len(self.array[index])) if self.array[index][i]]
197    
198    def __contains__(self, label: str) -> bool:
199        """ 
200        Args:
201            label (str): The vertex label.
202        Returns:
203            A boolean indicating if the vertex is in the graph.
204        """
205        return label in self.label_index
206    
207    def print_graph(self):
208        """ 
209        Print the contents of the graph.
210        """
211        print("   |", end="")
212        for label in self.labels:
213            print(f"{label:^3}", end=" ")
214        print()
215        print("----" * (len(self.array) + 1))
216        for r, row in enumerate(self.array):
217            label = self.labels[r]
218            print(f"{label:^3}|", end="")
219            for col in row:
220                b = " T " if col else "   "
221                print(b, end=" ")
222            print()
223            
224class AdjacencyMatrixWeightedGraph(AdjacencyMatrixGraph):
225    """ 
226    A weighted adjacency matrix graph implementation
227    (allows either directed or undirected representation)
228    vertex labels are string types
229    """
230    def __init__(self, labels):
231        """ 
232        Args:
233            labels: list of labels for each vertex (string types)
234        """
235        super().__init__(labels)
236
237    def add_edge(self, start_label: str, end_label: str, weight, directed=False):
238        """ 
239        Add an edge to the graph.
240
241        Args:
242            start_label (str): The starting vertex label.
243            end_label (str): The ending vertex label.
244            weight: The weight of the vertex.
245            directed: Whether the edge is directed.
246        """
247        a = self.label_index[start_label]
248        b = self.label_index[end_label]
249
250        self.array[a][b] = weight
251        self.array[a][a] = 0
252        self.array[b][b] = 0
253
254        if not directed:
255            self.array[b][a] = weight
256        
257    def print_graph(self):
258        """ 
259        Print the contents of the graph.
260        """
261        print("   |", end="")
262        for label in self.labels:
263            print(f"{label:>3}", end=" ")
264        print()
265        print("----" * (len(self.array) + 1))
266        for r, row in enumerate(self.array):
267            label = self.labels[r]
268            print(f"{label:^3}|", end="")
269            for col in row:
270                w = f"{col:3}" if col is not None else "   "
271                print(w, end=" ")
272            print()
273
274    def edges(self) -> list:
275        """ 
276        Return a list of edges in the graph. Each edge is represented by a tuple (start, end, weight).
277        """
278        edges = []
279        vertex_count = len(self.labels)
280        for i in range(vertex_count):
281            for j in range(vertex_count):
282                weight = self.array[i][j]
283                if weight and i != j:  
284                    edges.append((self.labels[i], self.labels[j], weight))
285    
286        return edges
287    
288    def undirected_edges(self) -> list:
289        """ 
290        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end, weight).
291        """
292        edges = []
293        vertex_count = len(self.labels)
294        for i in range(vertex_count):
295            for j in range(vertex_count):
296                weight = self.array[i][j]
297                if weight and i != j and (self.labels[j], self.labels[i], weight) not in edges:  
298                    edges.append((self.labels[i], self.labels[j], weight))
299    
300        return edges
301
302    def is_edge(self, start_label: str, end_label: str) -> bool:
303        """ 
304        Return boolean if an edge exists.
305
306        
307        Args:
308            start_label (str): Starting vertex label.
309            end_label (str): Ending vertex label.
310        
311        Returns:
312            A boolean of whether there is an edge from start to end.
313        """
314        return super().is_edge(start_label, end_label)
315    
316    def weightx(self, start: str, end: str) -> bool:
317        """ 
318        Return weight of an edge (is this used???)
319        Args:
320            start_label: starting vertex label
321            end_label: starting vertex label
322        
323        Returns:
324            weight value of an edge from start to end
325        """
326        return super().is_edge(start, end)
327
328    def __getitem__(self, vertex: str) -> dict:
329        """ 
330        Args:
331            vertex: vertex label
332        Returns:
333            a dictionary of adjacent vertex labels and weights
334        """
335        index = self.label_index[vertex]
336        return {self.labels[i] : self.array[index][i] for i in range(len(self.array[index])) if self.array[index][i]}
337
338class AdjacencyListGraph:
339    """ 
340    A unweighted adjacency list vertex implementation
341    (allows either directed or undirected representation)
342    vertex labels are string types
343    """
344    def __init__(self):
345        #: hash table of vertices in graph
346        self._adjacents = {}
347        
348    def add_edge(self, start_label: str, end_label: str, directed=False):
349        """ 
350        Add an edge to the graph.
351
352        Args:
353            start_label (str): The label of the starting vertex.
354            end_label (str): The label of the ending vertex.
355            directed (bool): Whether the edge is directed.
356        """
357        self.add_directed_edge(start_label, end_label)
358        if not directed:
359            self.add_directed_edge(end_label, start_label)
360                
361    def add_directed_edge(self, start_label: str, end_label: str):
362        """ 
363        Add a directed edge to the graph.
364
365        Args:
366            start_label (str): The label of the starting vertex.
367            end_label (str): The label of the ending vertex.
368        """
369        if start_label not in self._adjacents:
370            self._adjacents[start_label] = [end_label]
371        else:
372            if end_label not in self._adjacents[start_label]:
373                self._adjacents[start_label].append(end_label)
374        if end_label not in self._adjacents:
375            self._adjacents[end_label] = []
376
377    def delete_edge(self, start_label: str, end_label: str, directed=False):
378        """ 
379        Delete an edge in the graph.
380        
381        Args:
382            start_label (str): Starting vertex label.
383            end_label (str): Ending vertex label.
384            directed (bool): Whether the edge is directed.
385        Raises:
386            KeyError: If the edge does exist.
387        """
388        if start_label not in self._adjacents:
389            raise KeyError(f"Vertex {start_label} does not exist")
390        if end_label not in self._adjacents[start_label]:
391            raise KeyError(f"Vertex {end_label} does not exist")
392        self._adjacents[start_label].remove(end_label)
393        if not directed:
394            if end_label not in self._adjacents:
395                raise KeyError(f"Vertex {end_label} does not exist")
396            if start_label not in self._adjacents[end_label]:
397                raise KeyError(f"Vertex {start_label} does not exist")
398            self._adjacents[end_label].remove(start_label)
399
400    def vertices(self) -> list:
401        """
402        Return a list of vertex labels of the graph
403        """
404        return list(self._adjacents.keys())
405  
406    def adjacents(self, vertex: str) -> list:
407        """
408        Return a list of adjacents of a given vertex
409
410        Args:
411            vertex (str): The label of the vertex.
412        """
413        return self._adjacents[vertex]
414
415    def dfs_traverse(self, start_label: str):
416        """
417        Return a list of vertices through depth first traversal starting at a given vertex.
418
419        Args:
420            start_label (str): The starting vertex label.
421        Returns:
422            A list of vertex labels.
423        """
424        return self._df_rec_traverse(start_label, set(), [])
425
426    def _df_rec_traverse(self, start_label: str, visited, dflist):
427        """
428        Helper depth first traversal recursive function.
429        """
430        visited.add(start_label)
431        dflist.append(start_label)
432        
433        for v in self[start_label]:
434            if v not in visited:
435                self._df_rec_traverse(v, visited, dflist)
436        return dflist
437    
438    def bfs_traverse(self, start_label: str):
439        """
440        Return a list of vertices through breadth first traversal starting at a given vertex
441        Args:
442            start_label (str): The starting vertex label.
443        Returns:
444            A list of vertex labels.
445        """
446        visited = set()
447        q = Queue()
448        bflist = []
449
450        q.enqueue(start_label)
451        visited.add(start_label)
452        bflist.append(start_label)
453
454        while len(q) > 0:
455            current = q.dequeue()
456
457            for v in self[current]:
458                if v not in visited:               
459                    visited.add(v)
460                    q.enqueue(v)
461                    bflist.append(v)
462        return bflist
463        
464    def dfs(self, start_label:str, end_label:str) -> str:
465        """ 
466        Recursive depth first search.
467
468        Args:
469            start_label (str): The starting vertex label.
470            end_label (str): The vertex label to search for.
471
472        Returns:
473            A vertex in the graph if found, None otherwise.
474        """
475        def dfs_rec(current: str, end: str, visited):
476            if current == end:
477                return current
478
479            visited.add(current)
480
481            for v in self.adjacents(current):
482                if v not in visited:
483                    result = dfs_rec(v, end, visited)
484                    if result is not None:
485                        return result
486            
487            return None
488        
489        return dfs_rec(start_label, end_label, set())
490    
491    def bfs(self, start_label: str, end_label: str) -> str:
492        """ 
493        Breadth first search.
494
495        Args:
496            start_label (str): The starting vertex label.
497            end_label (str): The vertex label to search for.
498
499        Returns:
500            A vertex in the graph if found, None otherwise.
501        """
502        visited = set()
503        queue = Queue()
504        
505        visited.add(start_label)
506        queue.enqueue(start_label)
507
508        while len(queue) > 0:
509            current = queue.dequeue()
510            
511            if current == end_label:
512                return current
513            
514            for v in self[current]:
515                if v not in visited:               
516                    visited.add(v)
517                    queue.enqueue(v)
518        
519        return None
520    
521    def __getitem__(self, label: str):
522        """ 
523        Args:
524            vertex: vertex label
525        Returns:
526            a list of adjacent vertex labels
527        """
528
529        return self._adjacents[label]
530
531    def is_edge(self, start_label: str, end_label: str) -> bool:
532        """ 
533        Return boolean if an edge exists
534        Args:
535            start_label (str): The starting vertex label.
536            end_label (str): The ending vertex label.
537        
538        Returns:
539            A boolean of whether there is an edge from start to end
540        """
541        return end_label in self[start_label]
542
543    def __len__(self):
544        """
545        Return the number of nodes in the graph.
546        Returns:
547            int: The number of nodes in the graph.
548        """
549        return len(self._adjacents)
550
551    def __contains__(self, label: str) -> bool:
552        """ 
553        Args:
554            label (str): The vertex label.
555        Returns:
556            A boolean indicating if the vertex is in the graph.
557        """
558        return label in self._adjacents
559
560    def edges(self) -> list:
561        """ 
562        Return a list of edges in the graph. Each edge is represented by a tuple (start, end)
563        """
564        edges = []
565        for start in self._adjacents.keys():
566            for end in self.adjacents(start):
567                if start != end:  
568                    edges.append((start, end))
569        return edges
570
571    def undirected_edges(self) -> list:
572        """ 
573        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end)
574        """
575        edges = []
576        for start in self._adjacents.keys():
577            for end in self.adjacents(start):
578                if start != end and (end, start) not in edges:  
579                    edges.append((start, end))
580        return edges
581
582class AdjacencyListWeightedGraph(AdjacencyListGraph):
583    """ 
584    A weighted adjacency list vertex implementation in Python
585    (allows either directed or undirected representation)
586    """
587    def __init__(self):
588        #: hash table of vertices in graph
589        self._adjacents = {}
590        
591    def add_edge(self, start_label: str, end_label: str, weight, directed=False):
592        """ 
593        Add an edge to the graph.
594
595        Args:
596            start_label: The starting vertex label. 
597            end_label: The ending vertex label. 
598            weight: The edge weight.
599            directed: Whether the edge is directed.
600        """
601        if start_label not in self._adjacents:
602            self._adjacents[start_label] = {}
603            
604        self._adjacents[start_label][end_label] = weight
605
606        if not directed:
607            if end_label not in self._adjacents:
608                self._adjacents[end_label] = {}
609            self._adjacents[end_label][start_label] = weight
610
611    def delete_edge(self, start_label: str, end_label: str, directed=False):
612        """ 
613        Delete an edge in the graph.
614        
615        Args:
616            start_label (str): Starting vertex label.
617            end_label (str): Ending vertex label.
618            directed (bool): Whether the edge is directed.
619
620        Raises:
621            KeyError: If the edge does exist.
622        """
623        if start_label not in self._adjacents:
624            raise KeyError(f"Vertex {start_label} does not exist")
625        if end_label not in self._adjacents[start_label]:
626            raise KeyError(f"Vertex {end_label} does not exist") 
627        del self._adjacents[start_label][end_label]
628        if not directed:
629            if end_label not in self._adjacents:
630                raise KeyError(f"Vertex {end_label} does not exist")
631            if start_label not in self._adjacents[end_label]:
632                raise KeyError(f"Vertex {start_label} does not exist") 
633            del self._adjacents[end_label][start_label]
634
635    def adjacents(self, vertex: str) -> list:
636        """
637        Return a list of adjacents of a given vertex
638        Args:
639            vertex: starting vertex label 
640        """
641        return self._adjacents[vertex]
642
643    def dfs_traverse(self):
644        """
645        Perform depth first traversal.
646        """
647        return self._df_traverse_rec(self, set())
648
649    def _df_traverse_rec(self, vertex, visited=None):
650        """
651        helper depth first traversal recursive function
652        """
653        visited.add(vertex)
654        
655        for v in vertex.adjacents:
656            if v not in visited:
657                v._df_traverse_rec(v, visited)
658            
659    def bfs_traverse(self):
660        """
661        Perform breadth first traversal.
662        """
663        start = self
664        visited = set()
665        queue = Queue()
666        
667        queue.enqueue(start)
668
669        while len(queue) > 0:
670            current = queue.dequeue()
671            
672            if current not in visited:               
673                visited.add(current)
674        
675                for v in current.adjacents:
676                    queue.enqueue(v)
677        
678    def dfs(self, start_label: str, end_label: str) -> str:
679        """ 
680        Recursive depth first search.
681
682        Args:
683            start_label: The starting vertex label. 
684            end_label: The vertex label to search for. 
685        Returns:
686            vertex in the graph
687            none if not found.
688        """
689        return self.dfs_rec(start_label, end_label, set())
690        
691    def dfs_rec(self, current, end_label, visited=None):
692        """
693        Helper depth-first search recursive function.
694
695        Args:
696            current: Current vertex
697            end_label: Target vertex label
698            visited: Set of visited vertices 
699
700        Returns:
701            vertex in the graph if found, None otherwise.
702        """
703        if visited is None:
704            visited = set()
705        
706        if current == end_label:
707            return current
708
709        visited.add(current)
710        
711        for v in self.adjacents(current):
712            if v not in visited:
713                result = self.dfs_rec(v, end_label, visited)
714                if result is not None:
715                    return result
716        
717        return None
718
719
720    def bfs(self, start_label: str, end_label: str) -> str:
721        """ 
722        Recursive breadth first search.
723
724        Args:
725            end: vertex to search for
726
727        Returns:
728            vertex in the graph
729            None if not found.
730        """
731        visited = set()
732        queue = Queue()
733        
734        visited.add(start_label)
735        queue.enqueue(start_label)
736
737        while not queue.is_empty():
738            current = queue.dequeue()
739            
740            if current == end_label:
741                return current
742            
743            for v in self[current]:
744                if v not in visited:
745                    visited.add(v)
746                    queue.enqueue(v)
747        
748        return None
749
750    def __getitem__(self, vertex: str) -> dict:
751        """
752        Args:
753            vertex: vertex label
754        Returns:
755        Return a hashtable of adjacent vertices
756        """
757        if vertex not in self._adjacents:
758            return {}
759        return self._adjacents[vertex]
760
761    def __len__(self):
762        """
763        Return the number of nodes in the graph.
764
765        Returns:
766            int: The number of nodes in the graph.
767        """
768        return len(self._adjacents)
769
770    def edges(self) -> list:
771        """ 
772        Return a list of edges in the graph. Each edge is represented by a tuple (start, end, weight)
773        """
774        edges = []
775        for start in self._adjacents.keys():
776            for end in self.adjacents(start):
777                weight = self[start][end]
778                if start != end:  
779                    edges.append((start, end, weight))
780        return edges
781
782    def undirected_edges(self) -> list:
783        """ 
784        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end, weight)
785        """
786        edges = []
787        for start in self._adjacents.keys():
788            for end in self.adjacents(start):
789                weight = self[start][end]
790                if start != end and (end, start, weight) not in edges:  
791                    edges.append((start, end, weight))
792        return edges
793
794    def is_edge(self, start_label: str, end_label: str) -> bool:
795        """ 
796        Return boolean if an edge exists
797        Args:
798            start_label (str): starting vertex label
799            end_label (str): starting vertex label
800        
801        Returns:
802            A boolean of whether there is an edge from start to end.
803        """
804        return end_label in self[start_label]
805
806
807
808__pdoc__ = {
809    'AdjacencyMatrixGraph.add_vertex': False,
810    'AdjacencyMatrixGraph.delete_vertex': False,
811    'AdjacencyListGraph.add_directed_edge': False,
812}
class AdjacencyMatrixGraph:
  6class AdjacencyMatrixGraph:
  7    """ 
  8    An unweighted adjacency matrix graph implementation.
  9    
 10    This class allows either directed or undirected representation of a graph.
 11    Vertex labels are string types.
 12    """
 13    def __init__(self, labels: list[str]):
 14        """ 
 15        Initialize the graph with a list of vertex labels.
 16
 17        Args:
 18            labels (list[str]): List of labels for each vertex.
 19        """
 20        self.labels = labels
 21        self.label_index = { label: index for index, label  in enumerate(labels) }
 22
 23        node_count = len(self.labels)
 24        self.array = [[None for i in range(node_count)] for j in range(node_count)]
 25
 26    def add_edge(self, start_label: str, end_label: str, directed=False):
 27        """ 
 28        Add an edge in the graph.
 29        
 30        Args:
 31            start_label (str): Starting vertex label.
 32            end_label (str): Ending vertex label.
 33            directed (bool): Whether the edge is directed.
 34        """
 35        a = self.label_index[start_label]
 36        b = self.label_index[end_label]
 37        self.array[a][b] = True
 38        if not directed:
 39            self.array[b][a] = True
 40
 41    def add_vertex(self, label: str):
 42        """ 
 43        Add a vertex to the graph.
 44        
 45        Args:
 46            label (str): The vertex label to add.
 47        """
 48        self.labels.append(label)
 49        self.label_index[label] = len(self.labels) - 1
 50        for row in self.array:
 51            row.append(None)
 52        self.array.append([None for i in range(len(self.labels))])
 53
 54    def delete_vertex(self, label: str):
 55        """ 
 56        Delete a vertex from the graph.
 57        
 58        Args:
 59            label (str): The vertex label to delete.
 60        """
 61        index = self.label_index[label]
 62        self.labels.pop(index)
 63        self.label_index = { label: index for index, label in enumerate(self.labels) }
 64        self.array.pop(index)
 65        for row in self.array:
 66            row.pop(index)
 67
 68    def delete_edge(self, start_label: str, end_label: str, directed=False):
 69        """ 
 70        Delete an edge in the graph.
 71        
 72        Args:
 73            start_label (str): Starting vertex label.
 74            end_label (str): Ending vertex label.
 75            directed (bool): Whether the edge is directed.
 76        """
 77        a = self.label_index[start_label]
 78        b = self.label_index[end_label]
 79        if self.array[a][b] is None:
 80            raise KeyError(f"Edge {start_label} to {end_label} does not exist")
 81
 82        self.array[a][b] = None
 83        if not directed:
 84            if self.array[b][a] is None:
 85                raise KeyError(f"Edge {end_label} to {start_label} does not exist")
 86            self.array[b][a] = None
 87
 88    def dfs_traverse(self, start_label: str):
 89        """ 
 90        Perform depth first traversal in an adjacency matrix
 91 
 92        Args:
 93            start_label (str): Starting vertex label.
 94        
 95        Returns:
 96            Array with depth first order traversal.
 97        """
 98        return self._df_rec_traverse(start_label, set(), [])
 99        
100    def _df_rec_traverse(self, start_label: str, visited, dfs):
101        """ 
102        Helper method for depth first recursive traversal
103        """
104        start_index = self.label_index[start_label]
105        visited.add(start_label)
106        dfs.append(self.labels[start_index])
107
108        for adj_index, is_connected in enumerate(self.array[start_index]):
109            adj_label = self.labels[adj_index]
110            if is_connected and adj_label not in visited:
111                self._df_rec_traverse(adj_label, visited, dfs)
112
113        return dfs
114
115    def bfs_traverse(self, start_label: str):
116        """ 
117        Perform breadth first traversal in an adjacency matrix.
118 
119        Args:
120            start_label (str): Starting vertex label.
121        
122        Returns:
123            Array with breadth first order traversal.
124        """
125        bfs = []
126        q = Queue()
127        visited = set()
128
129        start_index = self.label_index[start_label]
130        q.enqueue(start_index)
131        bfs.append(self.labels[start_index])
132        while not q.is_empty():
133            current = q.dequeue()
134            for i in range(len(self.array)):
135                if start_index != i and self.array[current][i] and (i not in visited):
136                    visited.add(i)
137                    q.enqueue(i)
138                    bfs.append(self.labels[i])
139        return bfs
140    
141    def vertices(self) -> list:
142        """
143        Return a list of vertex labels of the graph
144        """
145        return self.labels
146    
147    def edges(self) -> list:
148        """ 
149        Return a list of edges in the graph. Each edge is represented by a tuple (start, end)
150        """
151        edges = []
152        vertex_count = len(self.labels)
153        for i in range(vertex_count):
154            for j in range(vertex_count):
155                if self.array[i][j] and i != j:  
156                    edges.append((self.labels[i], self.labels[j]))
157    
158        return edges
159
160    def undirected_edges(self) -> list:
161        """ 
162        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end)
163        """
164        edges = []
165        vertex_count = len(self.labels)
166        for i in range(vertex_count):
167            for j in range(vertex_count):
168                if self.array[i][j] and i != j and (self.labels[j], self.labels[i]) not in edges:  
169                    edges.append((self.labels[i], self.labels[j]))
170    
171        return edges
172
173    def is_edge(self, start_label: str, end_label: str) -> bool:
174        """ 
175        Return boolean if an edge exists.
176
177        Args:
178            start_label (str): starting vertex label
179            end_label (str): starting vertex label
180        
181        Returns:0
182            A boolean of whether there is an edge from start to end.
183        """
184        start_index = self.label_index[start_label]
185        end_index = self.label_index[end_label]
186
187        return self.array[start_index][end_index] is not None
188
189    def __getitem__(self, vertex: str) -> list:
190        """ 
191        Args:
192            vertex (str): The vertex label.
193        Returns:
194            A list of adjacent vertex labels.
195        """
196        index = self.label_index[vertex]
197        return [self.labels[i] for i in range(len(self.array[index])) if self.array[index][i]]
198    
199    def __contains__(self, label: str) -> bool:
200        """ 
201        Args:
202            label (str): The vertex label.
203        Returns:
204            A boolean indicating if the vertex is in the graph.
205        """
206        return label in self.label_index
207    
208    def print_graph(self):
209        """ 
210        Print the contents of the graph.
211        """
212        print("   |", end="")
213        for label in self.labels:
214            print(f"{label:^3}", end=" ")
215        print()
216        print("----" * (len(self.array) + 1))
217        for r, row in enumerate(self.array):
218            label = self.labels[r]
219            print(f"{label:^3}|", end="")
220            for col in row:
221                b = " T " if col else "   "
222                print(b, end=" ")
223            print()

An unweighted adjacency matrix graph implementation.

This class allows either directed or undirected representation of a graph. Vertex labels are string types.

AdjacencyMatrixGraph(labels: list[str])
13    def __init__(self, labels: list[str]):
14        """ 
15        Initialize the graph with a list of vertex labels.
16
17        Args:
18            labels (list[str]): List of labels for each vertex.
19        """
20        self.labels = labels
21        self.label_index = { label: index for index, label  in enumerate(labels) }
22
23        node_count = len(self.labels)
24        self.array = [[None for i in range(node_count)] for j in range(node_count)]

Initialize the graph with a list of vertex labels.

Args: labels (list[str]): List of labels for each vertex.

labels
label_index
array
def add_edge(self, start_label: str, end_label: str, directed=False):
26    def add_edge(self, start_label: str, end_label: str, directed=False):
27        """ 
28        Add an edge in the graph.
29        
30        Args:
31            start_label (str): Starting vertex label.
32            end_label (str): Ending vertex label.
33            directed (bool): Whether the edge is directed.
34        """
35        a = self.label_index[start_label]
36        b = self.label_index[end_label]
37        self.array[a][b] = True
38        if not directed:
39            self.array[b][a] = True

Add an edge in the graph.

Args: start_label (str): Starting vertex label. end_label (str): Ending vertex label. directed (bool): Whether the edge is directed.

def add_vertex(self, label: str):
41    def add_vertex(self, label: str):
42        """ 
43        Add a vertex to the graph.
44        
45        Args:
46            label (str): The vertex label to add.
47        """
48        self.labels.append(label)
49        self.label_index[label] = len(self.labels) - 1
50        for row in self.array:
51            row.append(None)
52        self.array.append([None for i in range(len(self.labels))])

Add a vertex to the graph.

Args: label (str): The vertex label to add.

def delete_vertex(self, label: str):
54    def delete_vertex(self, label: str):
55        """ 
56        Delete a vertex from the graph.
57        
58        Args:
59            label (str): The vertex label to delete.
60        """
61        index = self.label_index[label]
62        self.labels.pop(index)
63        self.label_index = { label: index for index, label in enumerate(self.labels) }
64        self.array.pop(index)
65        for row in self.array:
66            row.pop(index)

Delete a vertex from the graph.

Args: label (str): The vertex label to delete.

def delete_edge(self, start_label: str, end_label: str, directed=False):
68    def delete_edge(self, start_label: str, end_label: str, directed=False):
69        """ 
70        Delete an edge in the graph.
71        
72        Args:
73            start_label (str): Starting vertex label.
74            end_label (str): Ending vertex label.
75            directed (bool): Whether the edge is directed.
76        """
77        a = self.label_index[start_label]
78        b = self.label_index[end_label]
79        if self.array[a][b] is None:
80            raise KeyError(f"Edge {start_label} to {end_label} does not exist")
81
82        self.array[a][b] = None
83        if not directed:
84            if self.array[b][a] is None:
85                raise KeyError(f"Edge {end_label} to {start_label} does not exist")
86            self.array[b][a] = None

Delete an edge in the graph.

Args: start_label (str): Starting vertex label. end_label (str): Ending vertex label. directed (bool): Whether the edge is directed.

def dfs_traverse(self, start_label: str):
88    def dfs_traverse(self, start_label: str):
89        """ 
90        Perform depth first traversal in an adjacency matrix
91 
92        Args:
93            start_label (str): Starting vertex label.
94        
95        Returns:
96            Array with depth first order traversal.
97        """
98        return self._df_rec_traverse(start_label, set(), [])

Perform depth first traversal in an adjacency matrix

Args: start_label (str): Starting vertex label.

Returns: Array with depth first order traversal.

def bfs_traverse(self, start_label: str):
115    def bfs_traverse(self, start_label: str):
116        """ 
117        Perform breadth first traversal in an adjacency matrix.
118 
119        Args:
120            start_label (str): Starting vertex label.
121        
122        Returns:
123            Array with breadth first order traversal.
124        """
125        bfs = []
126        q = Queue()
127        visited = set()
128
129        start_index = self.label_index[start_label]
130        q.enqueue(start_index)
131        bfs.append(self.labels[start_index])
132        while not q.is_empty():
133            current = q.dequeue()
134            for i in range(len(self.array)):
135                if start_index != i and self.array[current][i] and (i not in visited):
136                    visited.add(i)
137                    q.enqueue(i)
138                    bfs.append(self.labels[i])
139        return bfs

Perform breadth first traversal in an adjacency matrix.

Args: start_label (str): Starting vertex label.

Returns: Array with breadth first order traversal.

def vertices(self) -> list:
141    def vertices(self) -> list:
142        """
143        Return a list of vertex labels of the graph
144        """
145        return self.labels

Return a list of vertex labels of the graph

def edges(self) -> list:
147    def edges(self) -> list:
148        """ 
149        Return a list of edges in the graph. Each edge is represented by a tuple (start, end)
150        """
151        edges = []
152        vertex_count = len(self.labels)
153        for i in range(vertex_count):
154            for j in range(vertex_count):
155                if self.array[i][j] and i != j:  
156                    edges.append((self.labels[i], self.labels[j]))
157    
158        return edges

Return a list of edges in the graph. Each edge is represented by a tuple (start, end)

def undirected_edges(self) -> list:
160    def undirected_edges(self) -> list:
161        """ 
162        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end)
163        """
164        edges = []
165        vertex_count = len(self.labels)
166        for i in range(vertex_count):
167            for j in range(vertex_count):
168                if self.array[i][j] and i != j and (self.labels[j], self.labels[i]) not in edges:  
169                    edges.append((self.labels[i], self.labels[j]))
170    
171        return edges

Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end)

def is_edge(self, start_label: str, end_label: str) -> bool:
173    def is_edge(self, start_label: str, end_label: str) -> bool:
174        """ 
175        Return boolean if an edge exists.
176
177        Args:
178            start_label (str): starting vertex label
179            end_label (str): starting vertex label
180        
181        Returns:0
182            A boolean of whether there is an edge from start to end.
183        """
184        start_index = self.label_index[start_label]
185        end_index = self.label_index[end_label]
186
187        return self.array[start_index][end_index] is not None

Return boolean if an edge exists.

Args: start_label (str): starting vertex label end_label (str): starting vertex label

Returns:0 A boolean of whether there is an edge from start to end.

def print_graph(self):
208    def print_graph(self):
209        """ 
210        Print the contents of the graph.
211        """
212        print("   |", end="")
213        for label in self.labels:
214            print(f"{label:^3}", end=" ")
215        print()
216        print("----" * (len(self.array) + 1))
217        for r, row in enumerate(self.array):
218            label = self.labels[r]
219            print(f"{label:^3}|", end="")
220            for col in row:
221                b = " T " if col else "   "
222                print(b, end=" ")
223            print()

Print the contents of the graph.

class AdjacencyMatrixWeightedGraph(AdjacencyMatrixGraph):
225class AdjacencyMatrixWeightedGraph(AdjacencyMatrixGraph):
226    """ 
227    A weighted adjacency matrix graph implementation
228    (allows either directed or undirected representation)
229    vertex labels are string types
230    """
231    def __init__(self, labels):
232        """ 
233        Args:
234            labels: list of labels for each vertex (string types)
235        """
236        super().__init__(labels)
237
238    def add_edge(self, start_label: str, end_label: str, weight, directed=False):
239        """ 
240        Add an edge to the graph.
241
242        Args:
243            start_label (str): The starting vertex label.
244            end_label (str): The ending vertex label.
245            weight: The weight of the vertex.
246            directed: Whether the edge is directed.
247        """
248        a = self.label_index[start_label]
249        b = self.label_index[end_label]
250
251        self.array[a][b] = weight
252        self.array[a][a] = 0
253        self.array[b][b] = 0
254
255        if not directed:
256            self.array[b][a] = weight
257        
258    def print_graph(self):
259        """ 
260        Print the contents of the graph.
261        """
262        print("   |", end="")
263        for label in self.labels:
264            print(f"{label:>3}", end=" ")
265        print()
266        print("----" * (len(self.array) + 1))
267        for r, row in enumerate(self.array):
268            label = self.labels[r]
269            print(f"{label:^3}|", end="")
270            for col in row:
271                w = f"{col:3}" if col is not None else "   "
272                print(w, end=" ")
273            print()
274
275    def edges(self) -> list:
276        """ 
277        Return a list of edges in the graph. Each edge is represented by a tuple (start, end, weight).
278        """
279        edges = []
280        vertex_count = len(self.labels)
281        for i in range(vertex_count):
282            for j in range(vertex_count):
283                weight = self.array[i][j]
284                if weight and i != j:  
285                    edges.append((self.labels[i], self.labels[j], weight))
286    
287        return edges
288    
289    def undirected_edges(self) -> list:
290        """ 
291        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end, weight).
292        """
293        edges = []
294        vertex_count = len(self.labels)
295        for i in range(vertex_count):
296            for j in range(vertex_count):
297                weight = self.array[i][j]
298                if weight and i != j and (self.labels[j], self.labels[i], weight) not in edges:  
299                    edges.append((self.labels[i], self.labels[j], weight))
300    
301        return edges
302
303    def is_edge(self, start_label: str, end_label: str) -> bool:
304        """ 
305        Return boolean if an edge exists.
306
307        
308        Args:
309            start_label (str): Starting vertex label.
310            end_label (str): Ending vertex label.
311        
312        Returns:
313            A boolean of whether there is an edge from start to end.
314        """
315        return super().is_edge(start_label, end_label)
316    
317    def weightx(self, start: str, end: str) -> bool:
318        """ 
319        Return weight of an edge (is this used???)
320        Args:
321            start_label: starting vertex label
322            end_label: starting vertex label
323        
324        Returns:
325            weight value of an edge from start to end
326        """
327        return super().is_edge(start, end)
328
329    def __getitem__(self, vertex: str) -> dict:
330        """ 
331        Args:
332            vertex: vertex label
333        Returns:
334            a dictionary of adjacent vertex labels and weights
335        """
336        index = self.label_index[vertex]
337        return {self.labels[i] : self.array[index][i] for i in range(len(self.array[index])) if self.array[index][i]}

A weighted adjacency matrix graph implementation (allows either directed or undirected representation) vertex labels are string types

AdjacencyMatrixWeightedGraph(labels)
231    def __init__(self, labels):
232        """ 
233        Args:
234            labels: list of labels for each vertex (string types)
235        """
236        super().__init__(labels)

Args: labels: list of labels for each vertex (string types)

def add_edge(self, start_label: str, end_label: str, weight, directed=False):
238    def add_edge(self, start_label: str, end_label: str, weight, directed=False):
239        """ 
240        Add an edge to the graph.
241
242        Args:
243            start_label (str): The starting vertex label.
244            end_label (str): The ending vertex label.
245            weight: The weight of the vertex.
246            directed: Whether the edge is directed.
247        """
248        a = self.label_index[start_label]
249        b = self.label_index[end_label]
250
251        self.array[a][b] = weight
252        self.array[a][a] = 0
253        self.array[b][b] = 0
254
255        if not directed:
256            self.array[b][a] = weight

Add an edge to the graph.

Args: start_label (str): The starting vertex label. end_label (str): The ending vertex label. weight: The weight of the vertex. directed: Whether the edge is directed.

def print_graph(self):
258    def print_graph(self):
259        """ 
260        Print the contents of the graph.
261        """
262        print("   |", end="")
263        for label in self.labels:
264            print(f"{label:>3}", end=" ")
265        print()
266        print("----" * (len(self.array) + 1))
267        for r, row in enumerate(self.array):
268            label = self.labels[r]
269            print(f"{label:^3}|", end="")
270            for col in row:
271                w = f"{col:3}" if col is not None else "   "
272                print(w, end=" ")
273            print()

Print the contents of the graph.

def edges(self) -> list:
275    def edges(self) -> list:
276        """ 
277        Return a list of edges in the graph. Each edge is represented by a tuple (start, end, weight).
278        """
279        edges = []
280        vertex_count = len(self.labels)
281        for i in range(vertex_count):
282            for j in range(vertex_count):
283                weight = self.array[i][j]
284                if weight and i != j:  
285                    edges.append((self.labels[i], self.labels[j], weight))
286    
287        return edges

Return a list of edges in the graph. Each edge is represented by a tuple (start, end, weight).

def undirected_edges(self) -> list:
289    def undirected_edges(self) -> list:
290        """ 
291        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end, weight).
292        """
293        edges = []
294        vertex_count = len(self.labels)
295        for i in range(vertex_count):
296            for j in range(vertex_count):
297                weight = self.array[i][j]
298                if weight and i != j and (self.labels[j], self.labels[i], weight) not in edges:  
299                    edges.append((self.labels[i], self.labels[j], weight))
300    
301        return edges

Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end, weight).

def is_edge(self, start_label: str, end_label: str) -> bool:
303    def is_edge(self, start_label: str, end_label: str) -> bool:
304        """ 
305        Return boolean if an edge exists.
306
307        
308        Args:
309            start_label (str): Starting vertex label.
310            end_label (str): Ending vertex label.
311        
312        Returns:
313            A boolean of whether there is an edge from start to end.
314        """
315        return super().is_edge(start_label, end_label)

Return boolean if an edge exists.

Args: start_label (str): Starting vertex label. end_label (str): Ending vertex label.

Returns: A boolean of whether there is an edge from start to end.

def weightx(self, start: str, end: str) -> bool:
317    def weightx(self, start: str, end: str) -> bool:
318        """ 
319        Return weight of an edge (is this used???)
320        Args:
321            start_label: starting vertex label
322            end_label: starting vertex label
323        
324        Returns:
325            weight value of an edge from start to end
326        """
327        return super().is_edge(start, end)

Return weight of an edge (is this used???) Args: start_label: starting vertex label end_label: starting vertex label

Returns: weight value of an edge from start to end

class AdjacencyListGraph:
339class AdjacencyListGraph:
340    """ 
341    A unweighted adjacency list vertex implementation
342    (allows either directed or undirected representation)
343    vertex labels are string types
344    """
345    def __init__(self):
346        #: hash table of vertices in graph
347        self._adjacents = {}
348        
349    def add_edge(self, start_label: str, end_label: str, directed=False):
350        """ 
351        Add an edge to the graph.
352
353        Args:
354            start_label (str): The label of the starting vertex.
355            end_label (str): The label of the ending vertex.
356            directed (bool): Whether the edge is directed.
357        """
358        self.add_directed_edge(start_label, end_label)
359        if not directed:
360            self.add_directed_edge(end_label, start_label)
361                
362    def add_directed_edge(self, start_label: str, end_label: str):
363        """ 
364        Add a directed edge to the graph.
365
366        Args:
367            start_label (str): The label of the starting vertex.
368            end_label (str): The label of the ending vertex.
369        """
370        if start_label not in self._adjacents:
371            self._adjacents[start_label] = [end_label]
372        else:
373            if end_label not in self._adjacents[start_label]:
374                self._adjacents[start_label].append(end_label)
375        if end_label not in self._adjacents:
376            self._adjacents[end_label] = []
377
378    def delete_edge(self, start_label: str, end_label: str, directed=False):
379        """ 
380        Delete an edge in the graph.
381        
382        Args:
383            start_label (str): Starting vertex label.
384            end_label (str): Ending vertex label.
385            directed (bool): Whether the edge is directed.
386        Raises:
387            KeyError: If the edge does exist.
388        """
389        if start_label not in self._adjacents:
390            raise KeyError(f"Vertex {start_label} does not exist")
391        if end_label not in self._adjacents[start_label]:
392            raise KeyError(f"Vertex {end_label} does not exist")
393        self._adjacents[start_label].remove(end_label)
394        if not directed:
395            if end_label not in self._adjacents:
396                raise KeyError(f"Vertex {end_label} does not exist")
397            if start_label not in self._adjacents[end_label]:
398                raise KeyError(f"Vertex {start_label} does not exist")
399            self._adjacents[end_label].remove(start_label)
400
401    def vertices(self) -> list:
402        """
403        Return a list of vertex labels of the graph
404        """
405        return list(self._adjacents.keys())
406  
407    def adjacents(self, vertex: str) -> list:
408        """
409        Return a list of adjacents of a given vertex
410
411        Args:
412            vertex (str): The label of the vertex.
413        """
414        return self._adjacents[vertex]
415
416    def dfs_traverse(self, start_label: str):
417        """
418        Return a list of vertices through depth first traversal starting at a given vertex.
419
420        Args:
421            start_label (str): The starting vertex label.
422        Returns:
423            A list of vertex labels.
424        """
425        return self._df_rec_traverse(start_label, set(), [])
426
427    def _df_rec_traverse(self, start_label: str, visited, dflist):
428        """
429        Helper depth first traversal recursive function.
430        """
431        visited.add(start_label)
432        dflist.append(start_label)
433        
434        for v in self[start_label]:
435            if v not in visited:
436                self._df_rec_traverse(v, visited, dflist)
437        return dflist
438    
439    def bfs_traverse(self, start_label: str):
440        """
441        Return a list of vertices through breadth first traversal starting at a given vertex
442        Args:
443            start_label (str): The starting vertex label.
444        Returns:
445            A list of vertex labels.
446        """
447        visited = set()
448        q = Queue()
449        bflist = []
450
451        q.enqueue(start_label)
452        visited.add(start_label)
453        bflist.append(start_label)
454
455        while len(q) > 0:
456            current = q.dequeue()
457
458            for v in self[current]:
459                if v not in visited:               
460                    visited.add(v)
461                    q.enqueue(v)
462                    bflist.append(v)
463        return bflist
464        
465    def dfs(self, start_label:str, end_label:str) -> str:
466        """ 
467        Recursive depth first search.
468
469        Args:
470            start_label (str): The starting vertex label.
471            end_label (str): The vertex label to search for.
472
473        Returns:
474            A vertex in the graph if found, None otherwise.
475        """
476        def dfs_rec(current: str, end: str, visited):
477            if current == end:
478                return current
479
480            visited.add(current)
481
482            for v in self.adjacents(current):
483                if v not in visited:
484                    result = dfs_rec(v, end, visited)
485                    if result is not None:
486                        return result
487            
488            return None
489        
490        return dfs_rec(start_label, end_label, set())
491    
492    def bfs(self, start_label: str, end_label: str) -> str:
493        """ 
494        Breadth first search.
495
496        Args:
497            start_label (str): The starting vertex label.
498            end_label (str): The vertex label to search for.
499
500        Returns:
501            A vertex in the graph if found, None otherwise.
502        """
503        visited = set()
504        queue = Queue()
505        
506        visited.add(start_label)
507        queue.enqueue(start_label)
508
509        while len(queue) > 0:
510            current = queue.dequeue()
511            
512            if current == end_label:
513                return current
514            
515            for v in self[current]:
516                if v not in visited:               
517                    visited.add(v)
518                    queue.enqueue(v)
519        
520        return None
521    
522    def __getitem__(self, label: str):
523        """ 
524        Args:
525            vertex: vertex label
526        Returns:
527            a list of adjacent vertex labels
528        """
529
530        return self._adjacents[label]
531
532    def is_edge(self, start_label: str, end_label: str) -> bool:
533        """ 
534        Return boolean if an edge exists
535        Args:
536            start_label (str): The starting vertex label.
537            end_label (str): The ending vertex label.
538        
539        Returns:
540            A boolean of whether there is an edge from start to end
541        """
542        return end_label in self[start_label]
543
544    def __len__(self):
545        """
546        Return the number of nodes in the graph.
547        Returns:
548            int: The number of nodes in the graph.
549        """
550        return len(self._adjacents)
551
552    def __contains__(self, label: str) -> bool:
553        """ 
554        Args:
555            label (str): The vertex label.
556        Returns:
557            A boolean indicating if the vertex is in the graph.
558        """
559        return label in self._adjacents
560
561    def edges(self) -> list:
562        """ 
563        Return a list of edges in the graph. Each edge is represented by a tuple (start, end)
564        """
565        edges = []
566        for start in self._adjacents.keys():
567            for end in self.adjacents(start):
568                if start != end:  
569                    edges.append((start, end))
570        return edges
571
572    def undirected_edges(self) -> list:
573        """ 
574        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end)
575        """
576        edges = []
577        for start in self._adjacents.keys():
578            for end in self.adjacents(start):
579                if start != end and (end, start) not in edges:  
580                    edges.append((start, end))
581        return edges

A unweighted adjacency list vertex implementation (allows either directed or undirected representation) vertex labels are string types

def add_edge(self, start_label: str, end_label: str, directed=False):
349    def add_edge(self, start_label: str, end_label: str, directed=False):
350        """ 
351        Add an edge to the graph.
352
353        Args:
354            start_label (str): The label of the starting vertex.
355            end_label (str): The label of the ending vertex.
356            directed (bool): Whether the edge is directed.
357        """
358        self.add_directed_edge(start_label, end_label)
359        if not directed:
360            self.add_directed_edge(end_label, start_label)

Add an edge to the graph.

Args: start_label (str): The label of the starting vertex. end_label (str): The label of the ending vertex. directed (bool): Whether the edge is directed.

def add_directed_edge(self, start_label: str, end_label: str):
362    def add_directed_edge(self, start_label: str, end_label: str):
363        """ 
364        Add a directed edge to the graph.
365
366        Args:
367            start_label (str): The label of the starting vertex.
368            end_label (str): The label of the ending vertex.
369        """
370        if start_label not in self._adjacents:
371            self._adjacents[start_label] = [end_label]
372        else:
373            if end_label not in self._adjacents[start_label]:
374                self._adjacents[start_label].append(end_label)
375        if end_label not in self._adjacents:
376            self._adjacents[end_label] = []

Add a directed edge to the graph.

Args: start_label (str): The label of the starting vertex. end_label (str): The label of the ending vertex.

def delete_edge(self, start_label: str, end_label: str, directed=False):
378    def delete_edge(self, start_label: str, end_label: str, directed=False):
379        """ 
380        Delete an edge in the graph.
381        
382        Args:
383            start_label (str): Starting vertex label.
384            end_label (str): Ending vertex label.
385            directed (bool): Whether the edge is directed.
386        Raises:
387            KeyError: If the edge does exist.
388        """
389        if start_label not in self._adjacents:
390            raise KeyError(f"Vertex {start_label} does not exist")
391        if end_label not in self._adjacents[start_label]:
392            raise KeyError(f"Vertex {end_label} does not exist")
393        self._adjacents[start_label].remove(end_label)
394        if not directed:
395            if end_label not in self._adjacents:
396                raise KeyError(f"Vertex {end_label} does not exist")
397            if start_label not in self._adjacents[end_label]:
398                raise KeyError(f"Vertex {start_label} does not exist")
399            self._adjacents[end_label].remove(start_label)

Delete an edge in the graph.

Args: start_label (str): Starting vertex label. end_label (str): Ending vertex label. directed (bool): Whether the edge is directed. Raises: KeyError: If the edge does exist.

def vertices(self) -> list:
401    def vertices(self) -> list:
402        """
403        Return a list of vertex labels of the graph
404        """
405        return list(self._adjacents.keys())

Return a list of vertex labels of the graph

def adjacents(self, vertex: str) -> list:
407    def adjacents(self, vertex: str) -> list:
408        """
409        Return a list of adjacents of a given vertex
410
411        Args:
412            vertex (str): The label of the vertex.
413        """
414        return self._adjacents[vertex]

Return a list of adjacents of a given vertex

Args: vertex (str): The label of the vertex.

def dfs_traverse(self, start_label: str):
416    def dfs_traverse(self, start_label: str):
417        """
418        Return a list of vertices through depth first traversal starting at a given vertex.
419
420        Args:
421            start_label (str): The starting vertex label.
422        Returns:
423            A list of vertex labels.
424        """
425        return self._df_rec_traverse(start_label, set(), [])

Return a list of vertices through depth first traversal starting at a given vertex.

Args: start_label (str): The starting vertex label. Returns: A list of vertex labels.

def bfs_traverse(self, start_label: str):
439    def bfs_traverse(self, start_label: str):
440        """
441        Return a list of vertices through breadth first traversal starting at a given vertex
442        Args:
443            start_label (str): The starting vertex label.
444        Returns:
445            A list of vertex labels.
446        """
447        visited = set()
448        q = Queue()
449        bflist = []
450
451        q.enqueue(start_label)
452        visited.add(start_label)
453        bflist.append(start_label)
454
455        while len(q) > 0:
456            current = q.dequeue()
457
458            for v in self[current]:
459                if v not in visited:               
460                    visited.add(v)
461                    q.enqueue(v)
462                    bflist.append(v)
463        return bflist

Return a list of vertices through breadth first traversal starting at a given vertex Args: start_label (str): The starting vertex label. Returns: A list of vertex labels.

def dfs(self, start_label: str, end_label: str) -> str:
465    def dfs(self, start_label:str, end_label:str) -> str:
466        """ 
467        Recursive depth first search.
468
469        Args:
470            start_label (str): The starting vertex label.
471            end_label (str): The vertex label to search for.
472
473        Returns:
474            A vertex in the graph if found, None otherwise.
475        """
476        def dfs_rec(current: str, end: str, visited):
477            if current == end:
478                return current
479
480            visited.add(current)
481
482            for v in self.adjacents(current):
483                if v not in visited:
484                    result = dfs_rec(v, end, visited)
485                    if result is not None:
486                        return result
487            
488            return None
489        
490        return dfs_rec(start_label, end_label, set())

Recursive depth first search.

Args: start_label (str): The starting vertex label. end_label (str): The vertex label to search for.

Returns: A vertex in the graph if found, None otherwise.

def bfs(self, start_label: str, end_label: str) -> str:
492    def bfs(self, start_label: str, end_label: str) -> str:
493        """ 
494        Breadth first search.
495
496        Args:
497            start_label (str): The starting vertex label.
498            end_label (str): The vertex label to search for.
499
500        Returns:
501            A vertex in the graph if found, None otherwise.
502        """
503        visited = set()
504        queue = Queue()
505        
506        visited.add(start_label)
507        queue.enqueue(start_label)
508
509        while len(queue) > 0:
510            current = queue.dequeue()
511            
512            if current == end_label:
513                return current
514            
515            for v in self[current]:
516                if v not in visited:               
517                    visited.add(v)
518                    queue.enqueue(v)
519        
520        return None

Breadth first search.

Args: start_label (str): The starting vertex label. end_label (str): The vertex label to search for.

Returns: A vertex in the graph if found, None otherwise.

def is_edge(self, start_label: str, end_label: str) -> bool:
532    def is_edge(self, start_label: str, end_label: str) -> bool:
533        """ 
534        Return boolean if an edge exists
535        Args:
536            start_label (str): The starting vertex label.
537            end_label (str): The ending vertex label.
538        
539        Returns:
540            A boolean of whether there is an edge from start to end
541        """
542        return end_label in self[start_label]

Return boolean if an edge exists Args: start_label (str): The starting vertex label. end_label (str): The ending vertex label.

Returns: A boolean of whether there is an edge from start to end

def edges(self) -> list:
561    def edges(self) -> list:
562        """ 
563        Return a list of edges in the graph. Each edge is represented by a tuple (start, end)
564        """
565        edges = []
566        for start in self._adjacents.keys():
567            for end in self.adjacents(start):
568                if start != end:  
569                    edges.append((start, end))
570        return edges

Return a list of edges in the graph. Each edge is represented by a tuple (start, end)

def undirected_edges(self) -> list:
572    def undirected_edges(self) -> list:
573        """ 
574        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end)
575        """
576        edges = []
577        for start in self._adjacents.keys():
578            for end in self.adjacents(start):
579                if start != end and (end, start) not in edges:  
580                    edges.append((start, end))
581        return edges

Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end)

class AdjacencyListWeightedGraph(AdjacencyListGraph):
583class AdjacencyListWeightedGraph(AdjacencyListGraph):
584    """ 
585    A weighted adjacency list vertex implementation in Python
586    (allows either directed or undirected representation)
587    """
588    def __init__(self):
589        #: hash table of vertices in graph
590        self._adjacents = {}
591        
592    def add_edge(self, start_label: str, end_label: str, weight, directed=False):
593        """ 
594        Add an edge to the graph.
595
596        Args:
597            start_label: The starting vertex label. 
598            end_label: The ending vertex label. 
599            weight: The edge weight.
600            directed: Whether the edge is directed.
601        """
602        if start_label not in self._adjacents:
603            self._adjacents[start_label] = {}
604            
605        self._adjacents[start_label][end_label] = weight
606
607        if not directed:
608            if end_label not in self._adjacents:
609                self._adjacents[end_label] = {}
610            self._adjacents[end_label][start_label] = weight
611
612    def delete_edge(self, start_label: str, end_label: str, directed=False):
613        """ 
614        Delete an edge in the graph.
615        
616        Args:
617            start_label (str): Starting vertex label.
618            end_label (str): Ending vertex label.
619            directed (bool): Whether the edge is directed.
620
621        Raises:
622            KeyError: If the edge does exist.
623        """
624        if start_label not in self._adjacents:
625            raise KeyError(f"Vertex {start_label} does not exist")
626        if end_label not in self._adjacents[start_label]:
627            raise KeyError(f"Vertex {end_label} does not exist") 
628        del self._adjacents[start_label][end_label]
629        if not directed:
630            if end_label not in self._adjacents:
631                raise KeyError(f"Vertex {end_label} does not exist")
632            if start_label not in self._adjacents[end_label]:
633                raise KeyError(f"Vertex {start_label} does not exist") 
634            del self._adjacents[end_label][start_label]
635
636    def adjacents(self, vertex: str) -> list:
637        """
638        Return a list of adjacents of a given vertex
639        Args:
640            vertex: starting vertex label 
641        """
642        return self._adjacents[vertex]
643
644    def dfs_traverse(self):
645        """
646        Perform depth first traversal.
647        """
648        return self._df_traverse_rec(self, set())
649
650    def _df_traverse_rec(self, vertex, visited=None):
651        """
652        helper depth first traversal recursive function
653        """
654        visited.add(vertex)
655        
656        for v in vertex.adjacents:
657            if v not in visited:
658                v._df_traverse_rec(v, visited)
659            
660    def bfs_traverse(self):
661        """
662        Perform breadth first traversal.
663        """
664        start = self
665        visited = set()
666        queue = Queue()
667        
668        queue.enqueue(start)
669
670        while len(queue) > 0:
671            current = queue.dequeue()
672            
673            if current not in visited:               
674                visited.add(current)
675        
676                for v in current.adjacents:
677                    queue.enqueue(v)
678        
679    def dfs(self, start_label: str, end_label: str) -> str:
680        """ 
681        Recursive depth first search.
682
683        Args:
684            start_label: The starting vertex label. 
685            end_label: The vertex label to search for. 
686        Returns:
687            vertex in the graph
688            none if not found.
689        """
690        return self.dfs_rec(start_label, end_label, set())
691        
692    def dfs_rec(self, current, end_label, visited=None):
693        """
694        Helper depth-first search recursive function.
695
696        Args:
697            current: Current vertex
698            end_label: Target vertex label
699            visited: Set of visited vertices 
700
701        Returns:
702            vertex in the graph if found, None otherwise.
703        """
704        if visited is None:
705            visited = set()
706        
707        if current == end_label:
708            return current
709
710        visited.add(current)
711        
712        for v in self.adjacents(current):
713            if v not in visited:
714                result = self.dfs_rec(v, end_label, visited)
715                if result is not None:
716                    return result
717        
718        return None
719
720
721    def bfs(self, start_label: str, end_label: str) -> str:
722        """ 
723        Recursive breadth first search.
724
725        Args:
726            end: vertex to search for
727
728        Returns:
729            vertex in the graph
730            None if not found.
731        """
732        visited = set()
733        queue = Queue()
734        
735        visited.add(start_label)
736        queue.enqueue(start_label)
737
738        while not queue.is_empty():
739            current = queue.dequeue()
740            
741            if current == end_label:
742                return current
743            
744            for v in self[current]:
745                if v not in visited:
746                    visited.add(v)
747                    queue.enqueue(v)
748        
749        return None
750
751    def __getitem__(self, vertex: str) -> dict:
752        """
753        Args:
754            vertex: vertex label
755        Returns:
756        Return a hashtable of adjacent vertices
757        """
758        if vertex not in self._adjacents:
759            return {}
760        return self._adjacents[vertex]
761
762    def __len__(self):
763        """
764        Return the number of nodes in the graph.
765
766        Returns:
767            int: The number of nodes in the graph.
768        """
769        return len(self._adjacents)
770
771    def edges(self) -> list:
772        """ 
773        Return a list of edges in the graph. Each edge is represented by a tuple (start, end, weight)
774        """
775        edges = []
776        for start in self._adjacents.keys():
777            for end in self.adjacents(start):
778                weight = self[start][end]
779                if start != end:  
780                    edges.append((start, end, weight))
781        return edges
782
783    def undirected_edges(self) -> list:
784        """ 
785        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end, weight)
786        """
787        edges = []
788        for start in self._adjacents.keys():
789            for end in self.adjacents(start):
790                weight = self[start][end]
791                if start != end and (end, start, weight) not in edges:  
792                    edges.append((start, end, weight))
793        return edges
794
795    def is_edge(self, start_label: str, end_label: str) -> bool:
796        """ 
797        Return boolean if an edge exists
798        Args:
799            start_label (str): starting vertex label
800            end_label (str): starting vertex label
801        
802        Returns:
803            A boolean of whether there is an edge from start to end.
804        """
805        return end_label in self[start_label]

A weighted adjacency list vertex implementation in Python (allows either directed or undirected representation)

def add_edge(self, start_label: str, end_label: str, weight, directed=False):
592    def add_edge(self, start_label: str, end_label: str, weight, directed=False):
593        """ 
594        Add an edge to the graph.
595
596        Args:
597            start_label: The starting vertex label. 
598            end_label: The ending vertex label. 
599            weight: The edge weight.
600            directed: Whether the edge is directed.
601        """
602        if start_label not in self._adjacents:
603            self._adjacents[start_label] = {}
604            
605        self._adjacents[start_label][end_label] = weight
606
607        if not directed:
608            if end_label not in self._adjacents:
609                self._adjacents[end_label] = {}
610            self._adjacents[end_label][start_label] = weight

Add an edge to the graph.

Args: start_label: The starting vertex label. end_label: The ending vertex label. weight: The edge weight. directed: Whether the edge is directed.

def delete_edge(self, start_label: str, end_label: str, directed=False):
612    def delete_edge(self, start_label: str, end_label: str, directed=False):
613        """ 
614        Delete an edge in the graph.
615        
616        Args:
617            start_label (str): Starting vertex label.
618            end_label (str): Ending vertex label.
619            directed (bool): Whether the edge is directed.
620
621        Raises:
622            KeyError: If the edge does exist.
623        """
624        if start_label not in self._adjacents:
625            raise KeyError(f"Vertex {start_label} does not exist")
626        if end_label not in self._adjacents[start_label]:
627            raise KeyError(f"Vertex {end_label} does not exist") 
628        del self._adjacents[start_label][end_label]
629        if not directed:
630            if end_label not in self._adjacents:
631                raise KeyError(f"Vertex {end_label} does not exist")
632            if start_label not in self._adjacents[end_label]:
633                raise KeyError(f"Vertex {start_label} does not exist") 
634            del self._adjacents[end_label][start_label]

Delete an edge in the graph.

Args: start_label (str): Starting vertex label. end_label (str): Ending vertex label. directed (bool): Whether the edge is directed.

Raises: KeyError: If the edge does exist.

def adjacents(self, vertex: str) -> list:
636    def adjacents(self, vertex: str) -> list:
637        """
638        Return a list of adjacents of a given vertex
639        Args:
640            vertex: starting vertex label 
641        """
642        return self._adjacents[vertex]

Return a list of adjacents of a given vertex Args: vertex: starting vertex label

def dfs_traverse(self):
644    def dfs_traverse(self):
645        """
646        Perform depth first traversal.
647        """
648        return self._df_traverse_rec(self, set())

Perform depth first traversal.

def bfs_traverse(self):
660    def bfs_traverse(self):
661        """
662        Perform breadth first traversal.
663        """
664        start = self
665        visited = set()
666        queue = Queue()
667        
668        queue.enqueue(start)
669
670        while len(queue) > 0:
671            current = queue.dequeue()
672            
673            if current not in visited:               
674                visited.add(current)
675        
676                for v in current.adjacents:
677                    queue.enqueue(v)

Perform breadth first traversal.

def dfs(self, start_label: str, end_label: str) -> str:
679    def dfs(self, start_label: str, end_label: str) -> str:
680        """ 
681        Recursive depth first search.
682
683        Args:
684            start_label: The starting vertex label. 
685            end_label: The vertex label to search for. 
686        Returns:
687            vertex in the graph
688            none if not found.
689        """
690        return self.dfs_rec(start_label, end_label, set())

Recursive depth first search.

Args: start_label: The starting vertex label. end_label: The vertex label to search for. Returns: vertex in the graph none if not found.

def dfs_rec(self, current, end_label, visited=None):
692    def dfs_rec(self, current, end_label, visited=None):
693        """
694        Helper depth-first search recursive function.
695
696        Args:
697            current: Current vertex
698            end_label: Target vertex label
699            visited: Set of visited vertices 
700
701        Returns:
702            vertex in the graph if found, None otherwise.
703        """
704        if visited is None:
705            visited = set()
706        
707        if current == end_label:
708            return current
709
710        visited.add(current)
711        
712        for v in self.adjacents(current):
713            if v not in visited:
714                result = self.dfs_rec(v, end_label, visited)
715                if result is not None:
716                    return result
717        
718        return None

Helper depth-first search recursive function.

Args: current: Current vertex end_label: Target vertex label visited: Set of visited vertices

Returns: vertex in the graph if found, None otherwise.

def bfs(self, start_label: str, end_label: str) -> str:
721    def bfs(self, start_label: str, end_label: str) -> str:
722        """ 
723        Recursive breadth first search.
724
725        Args:
726            end: vertex to search for
727
728        Returns:
729            vertex in the graph
730            None if not found.
731        """
732        visited = set()
733        queue = Queue()
734        
735        visited.add(start_label)
736        queue.enqueue(start_label)
737
738        while not queue.is_empty():
739            current = queue.dequeue()
740            
741            if current == end_label:
742                return current
743            
744            for v in self[current]:
745                if v not in visited:
746                    visited.add(v)
747                    queue.enqueue(v)
748        
749        return None

Recursive breadth first search.

Args: end: vertex to search for

Returns: vertex in the graph None if not found.

def edges(self) -> list:
771    def edges(self) -> list:
772        """ 
773        Return a list of edges in the graph. Each edge is represented by a tuple (start, end, weight)
774        """
775        edges = []
776        for start in self._adjacents.keys():
777            for end in self.adjacents(start):
778                weight = self[start][end]
779                if start != end:  
780                    edges.append((start, end, weight))
781        return edges

Return a list of edges in the graph. Each edge is represented by a tuple (start, end, weight)

def undirected_edges(self) -> list:
783    def undirected_edges(self) -> list:
784        """ 
785        Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end, weight)
786        """
787        edges = []
788        for start in self._adjacents.keys():
789            for end in self.adjacents(start):
790                weight = self[start][end]
791                if start != end and (end, start, weight) not in edges:  
792                    edges.append((start, end, weight))
793        return edges

Return a list of undirected edges in the graph. Each edge is represented by a tuple (start, end, weight)

def is_edge(self, start_label: str, end_label: str) -> bool:
795    def is_edge(self, start_label: str, end_label: str) -> bool:
796        """ 
797        Return boolean if an edge exists
798        Args:
799            start_label (str): starting vertex label
800            end_label (str): starting vertex label
801        
802        Returns:
803            A boolean of whether there is an edge from start to end.
804        """
805        return end_label in self[start_label]

Return boolean if an edge exists Args: start_label (str): starting vertex label end_label (str): starting vertex label

Returns: A boolean of whether there is an edge from start to end.