1. Introduction to Java
2. Reference Types and Semantics
3. Method Specifications and Testing
4. Loop Invariants
5. Analyzing Complexity
6. Recursion
7. Sorting Algorithms
8. Classes and Encapsulation
9. Interfaces and Polymorphism
10. Inheritance
11. Additional Java Features
12. Collections and Generics
13. Linked Data
14. Iterating over Data Structures
15. Stacks and Queues
16. Trees and their Iterators
17. Binary Search Trees
18. Heaps and Priority Queues
19. Sets and Maps
20. Hashing
21. Graphs
21. Graphs

21. Graphs

Up to this point in the course, the data structures that we have studied have had a particular predetermined structure. Linear data structures such as lists arrange their data sequentially, with each element “connected” to up to two other elements on its left and right. This linear structure enabled the fast, random access guarantee of dynamic arrays and provided a convenient framework for modeling stacks and queues. Next, we studied trees, which arrange their data hierarchically. Restricting to binary trees, each element had at most one “parent” and at most two “children”. We could impose additional invariants on these connections and the overall structure of the tree (giving rise to BSTs and heaps) to achieve good performance for certain search and update operations.

While these rigid structures work well for many applications, they are often insufficient to model the complex interconnectedness of real-world data.

Over the next few lectures, we will introduce the Graph ADT as a tool for modeling these sorts of interconnected data. Today, we’ll introduce some basic terminology to discuss and categorize graphs, and we’ll consider different strategies for modeling graphs using other data structures that we have seen. In the following two lectures, we will turn our attention to how we can systematically traverse and navigate graphs to answer some questions about their structures. This is a rich topic of which we will barely scratch the surface. Graphs play a more prominent role in an algorithms course (e.g., CS 4820), where more time is devoted to understanding which questions about graphs can be answered efficiently, and which have no known efficient solutions.

Defining Graphs

At a high level, graphs are an ADT that we use to model connections between different entities. This means that the data of a graph primarily consists of two pieces of information.

  1. We need to know the underlying set of objects that we will be relating. We call these objects the vertices, or the nodes, of the graph. For example, the users are the vertices in a social network, and addresses or cities are the vertices in a transportation network.

  2. For any pair of vertices, we need to know whether they are connected or not. If they are connected, we say that there is an edge, or arc, between these vertices. For example, friendships are modeled by edges in a social network and flights are modeled with edges in an airline’s route network.

Definition: Graph, Vertices, Edges

A graph, often denoted \(G\), consists of a set of vertices \(V\) and a set of edges \(E\), where the edges are a subset of all possible (ordered) pairs of vertices.

It is often helpful to draw a picture of a graph to help visualize its structure. In this picture, we represent the vertices as circles. We write labels within these circles to distinguish the different vertices. Then, we draw an arrow between two vertices if the graph includes an edge between them. For example, the following figure

represents a graph with six vertices, \[ V = \{a,b,c,d,e,f\}, \]

and with seven edges,

\[ E = \Big\{ (a,b), (a,c), (b,d), (b,e), (c,d), (d,e), (e,f) \Big\}. \]

Notice that each edge is an ordered pair, where the vertex that is listed first is the tail (non-pointy end) of the arrow and the vertex that is listed second is the head (pointy end) of the arrow. For example, the arrow from vertex \(b\) (its tail) to vertex \(d\) (its head) corresponds to the edge \((b,d)\), but would not be represented as an edge \((d,b)\) (which would model a connection starting at \(d\) and ending at \(b\)). Since each edge has a prescribed direction (or orientation), we call this a directed graph.

Definition: Directed Graph, Head, Tail

In a directed graph, each edge models a connection that exists in one direction between a pair of vertices, but not the other. For a directed edge \((u,v) \in E\), we refer to \(u \in V\) as the tail of the edge and \(v \in V\) as its head.

For example, a network of flights in a given time window is most naturally modeled as a directed graph since there may be a flight in one direction between two cities but not the other. Similarly, road networks (which may have one-way streets) and some social networks (where a user can “follow” a celebrity without the requirement that the celebrity “follows” them back) are naturally modeled by directed graphs. We will focus on directed graphs in CS 2110. Note that we can model bi-directional connections between a pair of vertices by including two directed edges:

Remark:

Many theoretical computer scientists and mathematicians prefer to use the word "arc" to describe directed connections and reserve the word "edge" for undirected connections. This convention isn't as popular in data structures, so we'll stick with the word "edge".

Other Graph Varieties

While we will restrict our attention to simple directed graphs in CS 2110, the area of graph theory is rich with many other variants of graphs. We briefly remark on some of these here so that you’ll be familiar with this terminology.

Undirected Graphs

While we have imposed the requirement that every edge in our graph works in one specified direction (giving a directed graph), an alternative is to allow every edge to work in both directions (giving an undirected graph). When we visualize undirected graphs, we connect vertices with line segments as opposed to arrows.

Self-Loops

In some circumstances (for example, modeling the state transitions of a character in a game), it makes sense to allow an edge (either directed or undirected) to connect a vertex back to itself.

We call such an edge a self-loop.

Multigraphs

In some circumstances, it might make sense to allow multiple parallel edges (directed or undirected) between the same pair of vertices. For example, this could model multiple flights between the same pair of cities that occur over the course of one day.

When we allow parallel edges in a graph, we often refer to it as a multigraph.

The presence of self-loops and parallel edges adds complexity when reasoning about graphs. For this reason, we won’t consider them in CS 2110. Instead, we will restrict our attention to simple directed graphs.

Definition: Simple Graph

A simple graph is one without self-loops or parallel edges.

Graph Substructures

Since graphs can grow to be very large (e.g., social network graphs include billions vertices and edges), it will be useful to have terminology to discuss smaller structures present within the graph.

Neighbors

First, when we discuss traversal of a graph, it will be useful to understand which vertices we can reach by starting at one specific vertex \(v\) and following an edge. We call the vertices that are reachable from \(v\) its (out-)neighbors, which together comprise its (out-)neighborhood.

Definition: Neighbors, Neighborhood, Degree

In a directed graph \(G = (V,E)\), we say that a vertex \(v \in V\) is a neighbor (sometimes, an out-neighbor) of a vertex \(u \in V\) if the edge \((u,v)\) belongs to \(E\).

The neighborhood (or out-neighborhood) of vertex \(u \in V\), often denoted \(N_G(u)\) is the set of all of \(u\)'s neighbors, \[ N_G(u) = \big\{ v \in V \colon (u,v) \in E \big\}. \] The degree (or out-degree) of vertex \(u \in V\), often denoted \(d_G(u)\) is the size of \(u\)'s neighborhood.

For example, the vertex \(c\) has neighborhood \(N_G(c) = \{a,d,e\}\) (so \(c\) has degree \(d_G(c) = 3\)) in the following graph.

Paths

Many problems on graphs involve finding a (hopefully short) way to navigate between a pair of vertices along their edges. For example, booking travel on an airline requires finding a collection of flights that take you from your starting city to your destination city. In a social network, we may wish to understand how a message spread to a particular person as it was shared between friends. The structure of interest in both of these problems is a linked sequence of edges which we call a path.

Definition: Path, Length, Source, Destination

A path \(P\) is a sequence of distinct contiguous edges in a graph \(G = (V,E)\), \[ P = \Big( (v_0, v_1), (v_1, v_2), \dots, (v_{\ell-1}, v_{\ell}) \Big) \] The head of each edge in the path is the same vertex as the tail of the subsequent edge.

The length of a path is the number of edges that it contains (\(\ell\)).

The tail of the first edge in the path (\(v_0\)) is its source, and the head of its last edge (\(v_\ell\)) is its destination.

Remark:

Some people prefer to call the object that we just described a walk and reserve the word path for a walk that "visits" each vertex at most once (so \(v_0, v_1, \dots, v_{\ell}\) are all distinct vertices). Other people prefer to call this object a path and use the term simple path to describe those paths that do not revisit vertices.

We’ll often use the notational shorthand \(v_0 \to v_1 \to \dots \to v_{\ell}\) to describe the edges of a path, since this is less clunky than writing all the brackets and parentheses. We’ll also use the notation \(v_0 \rightsquigarrow v_{\ell}\) to describe the source and destination of a path while suppressing its intermediary vertices. We can identify paths in a graph by choosing any vertex to be its source and then following arrows (in the correct direction) to travel between vertices until we reach the desired destination. For example, in the graph

\( f \to d \to g \to e \to c \) is a path with source \(f\) and destination \(c\) (i.e., an \(f \rightsquigarrow c\) path).

Cycles

Cycles are a special case of paths.

Definition: Cycle

A cycle is a path whose source is the same vertex as its destination.

In other words, a cycle loops back to connect to where it began. In the previous graph \( e \to b \to d \to g \to e \) is a cycle (which we can also represent \( b \to d \to g \to e \to b \), among other possibilities).

Labeled Graphs

Often, we may want to associate extra information with the vertices or edges in a graph. We can do this by labeling this information on our visualization of the graph. Vertex labels (in addition to distinguishing the vertices) can model various attributes such as a cost to visit a vertex. In upcoming lectures, we’ll associate extra information with vertices to keep track of our progress in different graph traversal algorithms.

It is also common to add labels to the edges in a graph. When these labels are numerical, we often refer to them as the costs or weights on the edge. These weights can model things such as a cost or time to traverse the edge or model some notion of “distance” between the connected vertices. For example, consider the following weighted graph:

In this weighted graph, the edge \((c,d)\) has weight (sometimes called length) 5. We can also talk about the weight/length of a path or cycle, which is the sum of the weights/lengths of all of its constituent edges. For example, the path \( a \to b \to d \to e \to f \) from \(a\) to \(f\) has length \( 4 + 8 + 7 + 1 = 20\). The shortest path from \(a\) to \(f\) is \(a \to b \to e \to f\) with length \(4 + 6 + 1 = 11\). We will return to the question of finding the shortest path in a weighted graph in two lectures.

Remark:

Caution: We've overloaded the term "length" for a path. When the edges in the graph are not labeled with weights, a path's length is the number of edges that it contains. However, when the edges are labeled with weights, we frequently use the word "length" to describe the sum of weights of the path's edges. Some people prefer to use the term path "weight" in this latter case, but this gets clunky in "shortest" path problems, a universal term that would be more aptly described as a "lightest" path problem if we adopt this weight terminology.

ADTs for Graphs

Now that we have introduced some of the terminology for formally describing graphs, we can think about how to model directed graphs as an abstract data type. Note that Java does not provide a canonical Graph ADT, so we will need to design one ourselves. We will actually define three related types that model various parts of the graph: the Graph itself, a Vertex in the graph, and an Edge in the graph. This separation will allow us to swap in different Vertex and Edge classes to the same Graph definition to model different varieties of graphs (e.g., weighted vs. unweighted graphs). We’ll start by considering these Vertex and Edge types before considering the behaviors of the Graph ADT.

The Edge ADT

The simplest of the three abstract data types will be the Edge. This will be a generic class that is parameterized by a Vertex type V. The only behaviors that we will require of an Edge are to return its tail() and head() vertices.

1
2
3
4
5
6
7
8
/** Models an edge in a directed graph. */
public interface Edge<V> {
  /** Returns the vertex where this edge starts. */
  V tail();

  /** Returns the vertex where this edge ends. */
  V head();
}
1
2
3
4
5
6
7
8
/** Models an edge in a directed graph. */
public interface Edge<V> {
  /** Returns the vertex where this edge starts. */
  V tail();

  /** Returns the vertex where this edge ends. */
  V head();
}

Classes that implement the Edge interface may wish to add additional fields and methods to model other properties such as the weight of the edge. This will allow clients to access and interact with these properties through the Edge (subtype) objects. For example, we can define the following class to represent an int-weighted directed edges:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** Models a non-negative int-weighted edge in a directed graph. */
public class WeightedEdge<V> implements Edge<V> {
  /** The vertex at the tail of this edge. */
  private final V tail;

  /** The vertex at the head of this edge. */
  private final V head;

  /** The weight of this edge. Must be >= 0. New edges default to weight 1. */
  private int weight;

  /** 
   * Constructs a new WeightedEdge with the given `tail` and `head` vertices and 
   * default weight 1. 
   */
  public WeightedEdge(V tail, V head) {
    this.tail = tail;
    this.head = head;
    weight = 1;
  }

  @Override
  public V tail() {
    return tail;
  }

  @Override
  public V head() {
    return head;
  }

  /** Returns the weight of this edge. */
  public int weight() {
    return weight;
  }

  /** Updates the weight of this edge to the given value. Requires `weight >= 0`. */
  public void setWeight(int weight) {
    assert weight >= 0;
    this.weight = weight;
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/** Models a non-negative int-weighted edge in a directed graph. */
public class WeightedEdge<V> implements Edge<V> {
  /** The vertex at the tail of this edge. */
  private final V tail;

  /** The vertex at the head of this edge. */
  private final V head;

  /** The weight of this edge. Must be >= 0. New edges default to weight 1. */
  private int weight;

  /** 
   * Constructs a new WeightedEdge with the given `tail` and `head` vertices and 
   * default weight 1. 
   */
  public WeightedEdge(V tail, V head) {
    this.tail = tail;
    this.head = head;
    weight = 1;
  }

  @Override
  public V tail() {
    return tail;
  }

  @Override
  public V head() {
    return head;
  }

  /** Returns the weight of this edge. */
  public int weight() {
    return weight;
  }

  /** Updates the weight of this edge to the given value. Requires `weight >= 0`. */
  public void setWeight(int weight) {
    assert weight >= 0;
    this.weight = weight;
  }
}

The Vertex ADT

Next, let’s consider the Vertex type. What behaviors should a Vertex provide? First, a Vertex should make accessible its most basic state, its associated label(). A Vertex should also be able to return information about its neighborhood, as this will enable us to traverse a graph by interacting with its vertices. What methods may be useful here? As an initial list, we’ll include:

Since some of these methods will require returning Edge objects (i.e., objects that are subtypes of the Edge interface), our Vertex type should be generic on an edge type E. We can ensure that E models an Edge using the type bound E extends Edge<?> (where here the wildcard <?> acknowledges that Edge is itself a generic type without introducing a cyclic dependency where the Vertex type is parameterized by the Edge type, which is parameterized by the Vertex type…). By using this generic type E in our method signatures, the client of our Vertex class can interact with custom methods of their Edge subclass without a need for casting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** Represents a vertex in a graph labeled with a String. */
public interface Vertex<E extends Edge<?>> {
  /** Returns this vertex's label. */
  String label();

  /** Returns the degree of this vertex. */
  int degree();

  /**
   * Returns whether there is an edge with this vertex as its tail and with a 
   * vertex labeled `headLabel` as its head.
   */
  boolean hasNeighbor(String headLabel);

  /** 
   * Returns the edge connecting this vertex to the vertex labeled with `headLabel`. 
   * Requires that `hasNeighbor(headLabel) == true`.
   */
  E edgeTo(String headLabel);

  /**
   * Returns an object supporting iteration over all the edges connecting this 
   * vertex to another vertex in the graph. This vertex serves as the "tail" 
   * vertex for each such edge.
   */
  Iterable<E> outgoingEdges();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** Represents a vertex in a graph labeled with a String. */
public interface Vertex<E extends Edge<?>> {
  /** Returns this vertex's label. */
  String label();

  /** Returns the degree of this vertex. */
  int degree();

  /**
   * Returns whether there is an edge with this vertex as its tail and with a 
   * vertex labeled `headLabel` as its head.
   */
  boolean hasNeighbor(String headLabel);

  /** 
   * Returns the edge connecting this vertex to the vertex labeled with `headLabel`. 
   * Requires that `hasNeighbor(headLabel) == true`.
   */
  E edgeTo(String headLabel);

  /**
   * Returns an object supporting iteration over all the edges connecting this 
   * vertex to another vertex in the graph. This vertex serves as the "tail" 
   * vertex for each such edge.
   */
  Iterable<E> outgoingEdges();
}

The Graph ADT

Now, let’s consider what behaviors we might wish for our Graph type to support. We’ll categorize these behaviors into 3 groups:

Query Methods

These methods return information about various aspects of the graph without modifying its state. Some questions that we may wish to ask about a graph are:

  1. How many vertices does it have?
  2. How many edges does it have?
  3. Does it have a vertex with a particular label?
  4. Does it have an edge between two particular vertices?
  5. What is the degree of a particular vertex?
  6. What is the weight (or some other property) of a particular vertex/edge?
  7. Is there a path between two particular vertices in the graph?
  8. What are properties of a path (perhaps the shortest path) between two vertices?
  9. Does the graph contain cycles?

In addition to asking for existence in Questions 3-4, we may also want to receive a reference to these Vertex or Edge objects. Questions 5-6 discuss properties of vertices/edges, so they are better relegated to the Vertex and Edge types (once we have Graph methods that can return Vertex and Edge objects). Questions 7-9 concern more complicated substructures of graphs (paths and cycles), so they are probably more well-suited for an external graph utilities class.

Mutation Methods

We may also want ways to modify the structure of a graph. This includes operations such as

  1. Adding another vertex to the graph.
  2. Adding another edge to the graph between two existing vertices.
  3. Modifying the weight (or some other property) of a vertex/edge.
  4. Removing an edge from the graph.
  5. Removing a vertex (and all of its incident edges) from the graph.

As with the query methods, 3 is probably more appropriate in the Vertex and/or Edge classes. In the lecture, we’ll only consider mutating methods that append new structure (vertices or edges) to a graph. We leave it as an exercise to add methods to support removing edges and/or vertices (see Exercise 21.7).

Iteration Methods

Finally, we will want methods that will enable iteration over various aspects of the graph. Our Vertex ADT already enables iteration over outgoing edges, but our Graph ADT will add support for iterating over all the vertices() or all the edges() in the graph.

Together, all of these behaviors give rise to the following Graph interface. Take some time to read through its specifications.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
 * Represents a mutable, directed, simple graph whose vertices are labeled by Strings. 
 */
public interface Graph<V extends Vertex<?>, E extends Edge<V>> {

  /* ***********************************************************************
   * Query methods                                                         *
   * ***********************************************************************/

  /** Returns the number of vertices in this graph. */
  int vertexCount();

  /** Returns the number of edges in this graph. */
  int edgeCount();

  /** Returns whether there is a vertex with the given `label` in this graph. */
  boolean hasVertex(String label);

  /** 
   * Returns the vertex with the given `label`. 
   * Requires that `hasVertex(label) == true`.
   */
  V getVertex(String label);

  /**
   * Returns whether there is an edge with tail labeled `tailLabel` and head 
   * labeled `headLabel` in this graph.
   */
  boolean hasEdge(String tailLabel, String headLabel);

  /**
   * Returns the edge from vertex `tailLabel` to vertex `headLabel`.
   * Requires that `hasEdge(tailLabel, headLabel) == true`.
   */
  E getEdge(String tailLabel, String headLabel);

  /* ***********************************************************************
   * Mutation methods                                                      *
   * ***********************************************************************/

  /**
   * Adds a vertex with the given `label` to this graph. Throws an 
   * `IllegalArgumentException` if there is already a vertex in the 
   * graph with this label.
   */
  void addVertex(String label);

  /**
   * Adds an edge between the vertices with the given labels and returns a 
   * reference to this new edge. Throws an IllegalArgumentException if either 
   * endpoint of this edge is not a vertex in this graph or if the graph already 
   * contains an edge between these endpoints.
   */
  E addEdge(String tailLabel, String headLabel);

  /* ***********************************************************************
   * Iteration methods                                                     *
   * ***********************************************************************/

  /** Returns an object supporting iteration over all the vertices in this graph. */
  Iterable<V> vertices();

  /** Returns an object supporting iteration over all the edges in this graph. */
  Iterable<E> edges();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
 * Represents a mutable, directed, simple graph whose vertices are labeled by Strings. 
 */
public interface Graph<V extends Vertex<?>, E extends Edge<V>> {

  /* ***********************************************************************
   * Query methods                                                         *
   * ***********************************************************************/

  /** Returns the number of vertices in this graph. */
  int vertexCount();

  /** Returns the number of edges in this graph. */
  int edgeCount();

  /** Returns whether there is a vertex with the given `label` in this graph. */
  boolean hasVertex(String label);

  /** 
   * Returns the vertex with the given `label`. 
   * Requires that `hasVertex(label) == true`.
   */
  V getVertex(String label);

  /**
   * Returns whether there is an edge with tail labeled `tailLabel` and head 
   * labeled `headLabel` in this graph.
   */
  boolean hasEdge(String tailLabel, String headLabel);

  /**
   * Returns the edge from vertex `tailLabel` to vertex `headLabel`.
   * Requires that `hasEdge(tailLabel, headLabel) == true`.
   */
  E getEdge(String tailLabel, String headLabel);

  /* ***********************************************************************
   * Mutation methods                                                      *
   * ***********************************************************************/

  /**
   * Adds a vertex with the given `label` to this graph. Throws an 
   * `IllegalArgumentException` if there is already a vertex in the 
   * graph with this label.
   */
  void addVertex(String label);

  /**
   * Adds an edge between the vertices with the given labels and returns a 
   * reference to this new edge. Throws an IllegalArgumentException if either 
   * endpoint of this edge is not a vertex in this graph or if the graph already 
   * contains an edge between these endpoints.
   */
  E addEdge(String tailLabel, String headLabel);

  /* ***********************************************************************
   * Iteration methods                                                     *
   * ***********************************************************************/

  /** Returns an object supporting iteration over all the vertices in this graph. */
  Iterable<V> vertices();

  /** Returns an object supporting iteration over all the edges in this graph. */
  Iterable<E> edges();
}

Graph Representations

Next, let’s consider how we can combine other data structures that we have learned about to implement the Graph ADT. We’ll consider (variants of) the two most popular ways to represent graphs, adjacency matrices and adjacency lists and discuss trade-offs between these approaches.

Adjacency Matrices

As we discussed earlier, each edge in a graph corresponds to an ordered pair of vertices, its endpoints. Therefore, the maximum number of edges that a graph can have is upper-bounded by \(|V|^2\) (more specifically, \(|V|^2 - |V|\) since we disallow self-loops in simple graphs), the number of these ordered pairs. We can imagine building a table (i.e., a 2D array or a matrix) that lets us look up whether a particular pair of vertices has an edge between them. The rows and columns of this matrix correspond to particular vertices. As an example, consider the following figure.

When the edges in the graph are weighted, so carry additional information, it often makes sense to store references to the edges themselves (or null for vertex pairs that are not connected by an edge) within this matrix.

Let’s implement our Graph interface to model a weighted directed graph using an adjacency matrix representation. Our implementation will be in the AdjMatrixGraph class. We’ll use our WeightedEdge class from earlier to model the graph’s edges. For the vertices, we’ll define a new AdjMatrixVertex class that implements our Vertex interface. To support some of the vertex behaviors (e.g., checking for the presence of a neighbor), our vertices will need access to the adjacency matrix, which will live in the AdjMatrixGraph class. Thus, it makes sense for AdjMatrixVertex to be a (non-static) inner class of AdjMatrixGraph.

1
2
3
4
5
6
/** Represents a directed graph with int-weighted edges using an adjacency matrix. */
public class AdjMatrixGraph implements Graph<AdjMatrixVertex, WeightedEdge<AdjMatrixVertex>> {

  /** Represents a vertex in this graph. */
  public class AdjMatrixVertex implements Vertex<WeightedEdge<AdjMatrixVertex>> { ... } 
}
1
2
3
4
5
6
/** Represents a directed graph with int-weighted edges using an adjacency matrix. */
public class AdjMatrixGraph implements Graph<AdjMatrixVertex, WeightedEdge<AdjMatrixVertex>> {

  /** Represents a vertex in this graph. */
  public class AdjMatrixVertex implements Vertex<WeightedEdge<AdjMatrixVertex>> { ... } 
}

Let’s consider what state our graph and vertices will need to store. First, our graph will need to store the adjacency matrix. Since our Graph ADT supports adding vertices, which will cause a resizing of the adjacency matrix, it makes sense to use dynamic arrays (i.e., ArrayLists) for its backing storage. Since the adjacency matrix is 2-dimensional, we will need a nested list structure, an ArrayList<ArrayList<WeightedEdge<AdjMatrixVertex>>> (wow, these generic types have gotten complicated!).

The client references vertices using their String labels, and our AdjMatrixGraph class will need a way to efficiently translate these labels into vertices. We can do this using a HashMap<String, AdjMatrixVertex>. Within the AdjMatrixVertex class, we’ll need to store this label to support the label() method. We’ll also need a way to keep track of which rows/columns in the adjacency matrix correspond to which vertices, which we can do through an int index field in the AdjMatrixVertex. Together, these fields allow us to track all of the state that we will need.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** Represents a directed graph with int-weighted edges using an adjacency matrix. */
public class AdjMatrixGraph implements Graph<AdjMatrixVertex, WeightedEdge<AdjMatrixVertex>> {

  /** Represents a vertex in this graph. */
  public class AdjMatrixVertex implements Vertex<WeightedEdge<AdjMatrixVertex>> { 
    /** The label of this vertex */
    private final String label;

    /** The index of this vertex in the edges 2D ArrayList */
    private final int index;
  } 

  /** A map associating the labels of the vertices with their AdjMatrixVertex objects. */
  private final HashMap<String, AdjMatrixVertex> vertices;

  /**
   * A 2D ArrayList whose entries are indexed by the vertex indices. The (i,j)'th entry is null if
   * the vertex indexed i does not have an edge to the vertex indexed j; otherwise, it contains a
   * reference to this edge.
   */
  private final ArrayList<ArrayList<WeightedEdge<AdjMatrixVertex>>> edges;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** Represents a directed graph with int-weighted edges using an adjacency matrix. */
public class AdjMatrixGraph implements Graph<AdjMatrixVertex, WeightedEdge<AdjMatrixVertex>> {

  /** Represents a vertex in this graph. */
  public class AdjMatrixVertex implements Vertex<WeightedEdge<AdjMatrixVertex>> { 
    /** The label of this vertex */
    private final String label;

    /** The index of this vertex in the edges 2D ArrayList */
    private final int index;
  } 

  /** A map associating the labels of the vertices with their AdjMatrixVertex objects. */
  private final HashMap<String, AdjMatrixVertex> vertices;

  /**
   * A 2D ArrayList whose entries are indexed by the vertex indices. The (i,j)'th entry is null if
   * the vertex indexed i does not have an edge to the vertex indexed j; otherwise, it contains a
   * reference to this edge.
   */
  private final ArrayList<ArrayList<WeightedEdge<AdjMatrixVertex>>> edges;
}

From here, most of the Vertex and Graph methods are fairly straightforward to implement. The full implementations are provided with the release code, but we encourage you to take some time to complete them yourself. We discuss the implementation of some of these methods below.

AdjMatrixVertex.degree()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public int degree() {
  int degree = 0;
  for (WeightedEdge<AdjMatrixVertex> e : edges.get(index)) {
    if (e != null) {
      degree += 1;
    }
  }
  return degree;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public int degree() {
  int degree = 0;
  for (WeightedEdge<AdjMatrixVertex> e : edges.get(index)) {
    if (e != null) {
      degree += 1;
    }
  }
  return degree;
}
All of the information about edges (so also neighborhoods) resides in the adjacency matrix, we must use this to calculate the degree of this vertex. Information about the outgoing edges (or lack thereof) from this vertex will be in the index'th row of the matrix, which we obtain by calling edges.get(index). We must iterate over this row (i.e., ArrayList). Each of its entries which is not null corresponds to one outgoing edge and contributes 1 to this vertex's degree. Overall, this degree computation runs in \(O(|V|)\) time.

AdjMatrixVertex.hasNeighbor()

1
2
3
4
5
@Override
public boolean hasNeighbor(String headLabel) {
  return hasVertex(headLabel)
          && edges.get(index).get(vertices.get(headLabel).index) != null;
}
1
2
3
4
5
@Override
public boolean hasNeighbor(String headLabel) {
  return hasVertex(headLabel)
          && edges.get(index).get(vertices.get(headLabel).index) != null;
}
To check whether the vertex labeled headLabel is a neighbor of this vertex, we must inspect one entry of the edges adjacency matrix to check for the presence of an edge. The "row index" in the adjacency matrix is the index of the head, this.index. The "column index" in the adjacency matrix is the index of the head, vertices.get(headLabel).index; we use the HashMap to locate the head vertex from its label and then access its index field. Thus, the adjacency matrix entry we need is edges.get(index).get(vertices.get(headLabel).index). Here, the \(O(1)\) random access guarantee of ArrayLists and the expected \(O(1)\) lookup in the vertices map results in an \(O(1)\) expected runtime of hasNeighbor(). Checking for the presence of a particular edge is very efficient with an adjacency matrix. Similar reasoning applies to the edgeTo() method.

AdjMatrixVertex.outgoingEdges()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public Iterable<WeightedEdge<AdjMatrixVertex>> outgoingEdges() {
  ArrayList<WeightedEdge<AdjMatrixVertex>> outEdges = new ArrayList<>();
  for (WeightedEdge<AdjMatrixVertex> e : edges.get(index)) {
    if (e != null) {
      outEdges.add(e);
    }
  }
  return outEdges;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public Iterable<WeightedEdge<AdjMatrixVertex>> outgoingEdges() {
  ArrayList<WeightedEdge<AdjMatrixVertex>> outEdges = new ArrayList<>();
  for (WeightedEdge<AdjMatrixVertex> e : edges.get(index)) {
    if (e != null) {
      outEdges.add(e);
    }
  }
  return outEdges;
}
Similar to degree() the only way to interact with the neighborhood of a vertex is to iterate over a full row of the adjacency matrix to locate its neighbors. This adds an \(O(|V|)\) overhead to producing this iterator.

AdjMatrixGraph.edgeCount()

1
2
3
4
5
6
7
8
@Override
public int edgeCount() {
  int count = 0;
  for (AdjMatrixVertex v : vertices()) {
    count += v.degree();
  }
  return count;
}
1
2
3
4
5
6
7
8
@Override
public int edgeCount() {
  int count = 0;
  for (AdjMatrixVertex v : vertices()) {
    count += v.degree();
  }
  return count;
}
Using our chosen fields, we must iterate over the entire edges matrix to count the edges. This is a (painfully slow) \(O(|V|^2)\) operation. Adding a separate field to keep track of the edgeCount can improve performance; see Exercise 21.6.

AdjMatrixGraph.hasEdge()

1
2
3
4
5
@Override
public boolean hasEdge(String tailLabel, String headLabel) {
  return hasVertex(tailLabel)
          && vertices.get(tailLabel).hasNeighbor(headLabel);
}
1
2
3
4
5
@Override
public boolean hasEdge(String tailLabel, String headLabel) {
  return hasVertex(tailLabel)
          && vertices.get(tailLabel).hasNeighbor(headLabel);
}
Here, we can leverage the AdjMatrixVertex.hasNeighbor() method that we already wrote. To check for the presence of an edge, we simply make sure that its tail vertex exists and then check whether this tail vertex has a neighbor labeled with headLabel. These checks are performed in \(O(1)\) time.

AdjMatrixGraph.addVertex()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override
public void addVertex(String label) {
  if (hasVertex(label)) {
    throw new IllegalArgumentException("Graph already contains vertex " + label);
  }
  int index = vertices.size();
  vertices.put(label, new AdjMatrixVertex(label, index));
  edges.add(new ArrayList<>());
  for (ArrayList<WeightedEdge<AdjMatrixVertex>> adjacencies : edges) {
    adjacencies.add(null);
    edges.getLast().add(null);
  }
  edges.getLast().removeLast();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override
public void addVertex(String label) {
  if (hasVertex(label)) {
    throw new IllegalArgumentException("Graph already contains vertex " + label);
  }
  int index = vertices.size();
  vertices.put(label, new AdjMatrixVertex(label, index));
  edges.add(new ArrayList<>());
  for (ArrayList<WeightedEdge<AdjMatrixVertex>> adjacencies : edges) {
    adjacencies.add(null);
    edges.getLast().add(null);
  }
  edges.getLast().removeLast();
}
Whenever we add a vertex to the graph, we must resize the adjacency matrix. This requires us to both add a new row and also to add one more entry on the end of each existing row. These operations require \(O(|V|)\) amortized time.

Benefits and Drawbacks

The primary benefit of the adjacency matrix representation is that the random access guarantee of dynamic arrays allows us fast, \(O(1)\) lookup of edges. An additional benefit, that we will not explore too deeply, is that representing connection information as a matrix allows us to leverage some ideas from linear algebra to recast some graph operations (such as searching for paths of particular lengths) as matrix multiplication, which has more efficient algorithms.

There are a few primary drawbacks of the adjacency matrix representation. First, it requires a lot of memory. Even in a very sparse graph (in which most of the possible edges are not present), the adjacency matrix will require \(O(|V|^2)\) storage, most of which simply indicates the absence of particular edges. Next, resizing the adjacency matrix when a new vertex is added to the graph is an expensive operation (especially in the worst case when the backing storage of the ArrayLists must be resized). Finally, “local search” operations (i.e., visiting all of the neighbors of a vertex) require us to iterate over an entire (likely mostly empty) row of the adjacency matrix. We’d like to have a more efficient way to directly access the neighbors of a vertex. Our second graph representation, an adjacency list, affords us this ability.

Adjacency Lists

Rather than having the graph store a centralized table of adjacency information, an adjacency list representation delegates to each of its vertices the responsibility of tracking their neighborhoods. Classically, we visualize an adjacency list representation as a linked list of vertices (the red, leftmost vertical list in the following figure), where each of these vertices has its own linked list storing all of its neighboring vertices (or all of its outgoing edges).

While linked lists offer a good visualization tool, they are not the best choice in practice. Operations such as querying for the presence of a particular neighbor (a common subroutine of many graph operations) would require a linear traversal of the neighbors list. Instead, we can use HashMaps in place of both “layers” of lists. The outer list of vertices can be replaced with a map associating vertex labels to Vertex objects (just as we had in our AdjMatrixGraph implementation). The inner list of neighbors/edges can be replaced with a map associating the tail vertex labels to Edge objects. This will provide many of the same \(O(1)\) lookup guarantees that we had for adjacency matrices.

Let’s formalize these ideas. We’ll define an AdjListGraph class that is parameterized by the same WeightedEdge class and a new AdjListVertex class. Since vertices in an adjacency list are responsible for tracking their own adjacencies, they will not need to access any fields of AdjListGraph. Thus, it makes sense for AdjListVertex to be a static nested class of AdjListGraph with its own HashMap<String, WeightedEdge<<AdjListVertex>> field (along with a String field for its label). As we noted above, the AdjListGraph class will need a HashMap<String, AdjListVertex> field to keep collect its vertices.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/** Represents a directed graph with int-weighted edges using an adjacency list. */
public class AdjListGraph implements Graph<AdjListVertex, WeightedEdge<AdjListVertex>> {

  /** Represents a vertex in this graph responsible for tracking its neighbors. */
  public static class AdjListVertex implements Vertex<WeightedEdge<AdjListVertex>> {

    /** The label of this vertex */
    String label;

    /** A map associating the labels of this vertex's neighbors with the edges connecting to them. */
    HashMap<String, WeightedEdge<AdjListVertex>> outEdges;
  }

  /** A map associating the labels of the vertices with their AdjListVertex objects. */
  HashMap<String, AdjListVertex> vertices;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/** Represents a directed graph with int-weighted edges using an adjacency list. */
public class AdjListGraph implements Graph<AdjListVertex, WeightedEdge<AdjListVertex>> {

  /** Represents a vertex in this graph responsible for tracking its neighbors. */
  public static class AdjListVertex implements Vertex<WeightedEdge<AdjListVertex>> {

    /** The label of this vertex */
    String label;

    /** A map associating the labels of this vertex's neighbors with the edges connecting to them. */
    HashMap<String, WeightedEdge<AdjListVertex>> outEdges;
  }

  /** A map associating the labels of the vertices with their AdjListVertex objects. */
  HashMap<String, AdjListVertex> vertices;
}

As with the AdjMatrixGraph, most of these methods have straightforward implementations now that we have settled on a state representation. The full implementations are provided with the release code, but we encourage you to take some time to complete them yourself. We again discuss the implementation of some of these methods below.

AdjListVertex.degree()

1
2
3
4
@Override
public int degree() {
  return outEdges.size();
}
1
2
3
4
@Override
public int degree() {
  return outEdges.size();
}
Since our vertex now stores a map with one entry per neighbor, we can obtain its degree by simply querying the size of this map, an \(O(1)\) operation.

AdjListVertex.hasNeighbor()

1
2
3
4
@Override
public boolean hasNeighbor(String headLabel) {
  return outEdges.containsKey(headLabel);
}
1
2
3
4
@Override
public boolean hasNeighbor(String headLabel) {
  return outEdges.containsKey(headLabel);
}
We can similarly use a single \(O(1)\) membership lookup in the vertex's outEdges map to check for the presence of a particular outgoing edge.

AdjListVertex.outgoingEdges()

1
2
3
4
@Override
public Iterable<WeightedEdge<AdjListVertex>> outgoingEdges() {
  return outEdges.values();
}
1
2
3
4
@Override
public Iterable<WeightedEdge<AdjListVertex>> outgoingEdges() {
  return outEdges.values();
}
Since each vertex now maintains its own collection of outgoing edges, obtaining an iterator over these edges can now be achieved with a single method call.

AdjListGraph.edgeCount()

1
2
3
4
5
6
7
8
@Override
public int edgeCount() {
  int count = 0;
  for (AdjListVertex v : vertices()) {
    count += v.degree();
  }
  return count;
}
1
2
3
4
5
6
7
8
@Override
public int edgeCount() {
  int count = 0;
  for (AdjListVertex v : vertices()) {
    count += v.degree();
  }
  return count;
}
Using our chosen fields, we must iterate over all of the vertices summing their degrees to count the edges. Now that degree() is an \(O(1)\) operation, this requires \(O(|V|)\) time. Just as with the adjacency matrix implementation, we can add a separate field to keep track of the edgeCount within AdjListGraph to improve this performance; see Exercise 21.6.

AdjListGraph.addVertex()

1
2
3
4
5
6
7
@Override
public void addVertex(String label) {
  if (hasVertex(label)) {
    throw new IllegalArgumentException("Graph already contains vertex " + label);
  }
  vertices.put(label, new AdjListVertex(label));
}
1
2
3
4
5
6
7
@Override
public void addVertex(String label) {
  if (hasVertex(label)) {
    throw new IllegalArgumentException("Graph already contains vertex " + label);
  }
  vertices.put(label, new AdjListVertex(label));
}
After checking to make sure this vertex is not already present, adding a vertex to an adjacency list representation amounts to put()ting an additional entry in the vertices map associated to a new AdjListVertex object (with an initially empty outEdges map). This is an \(O(1)\) operation, as is the addEdge() method which (after parameter checking) simply updates the head vertex's outEdges map.

Benefits and Drawbacks

There are many benefits of the adjacency list representation, in particular when we implement it using maps for fast lookup operations. Each of the Graph ADT operations has a runtime no worse than for our adjacency matrix representation, and many operations can be made faster since we can avoid iterating over entire rows of an adjacency matrix. In addition, the memory footprint of an adjacency list is smaller when there are not too many edges, \(O(|V|+|E|)\), as opposed to the \(O(|V|^2)\) of an adjacency matrix.

Remark:

Let's pause here for a second to further consider these two complexity classes \(O(|V|+|E|)\) and \(O(|V|^2)\). Since there are two variables involved \(|V|\) and \(|E|\), it is not immediately clear how to compare them. This will depend on how "dense" the edges are in the graph.

In a sparse graph, there are relatively few edges. Typically, we think of a sparse graph as having \(O(|V|)\) edges, so each vertex has a roughly constant number of neighbors. In this case \(O(|V|+|E|) = O(|V|)\), so the adjacency list representation uses significantly less storage for large graphs.

In a dense graph, there are many edges, closer to the maximum possible number. You can think of a dense graph as having \(O(|V|^2)\) edges. In this case, \(O(|V|+|E|) = O(|V|^2)\), so the memory gains of adjacency lists are not significant; in fact, the additional memory to keep track of many more references behind the list or map representations can often make the adjacency list representation larger.

The main drawbacks of adjacency lists largely fall beyond our scope. The more decentralized storage of their data and the use of more complicated data structures (one map for each vertex) can lead to worse cache performance than an adjacency matrix. Also, the lack of a global table of adjacency information can make some more complicated graph operations slower. For our purposes, we will prefer the adjacency list representation as we continue to study graphs over the next two lectures.

Main Takeaways:

  • Graphs are used to model connections, or edges, between members of a set called its vertices. Sometimes, the vertices or edges in a graph are labeled with additional information which we often call their weights.
  • In a directed graph, each edge works in only one direction, pointing from its head to its tail. The neighbors of a vertex are the other vertices that it connects to via edges, and its degree is the size of its neighborhood
  • We can link together edges in a graph to form paths and cycles, which are important concepts for discussing graph traversals.
  • There are different ways to model the state of a graph. An adjacency matrix stores all edge information in a central 2D table. An adjacency list representation makes each vertex responsible for keeping track of its neighborhood.
  • Adjacency lists implemented with hash maps have great performance, especially in sparse graphs which have relatively few edges.

Exercises

Exercise 21.1: Check Your Understanding
Consider the following graph.
(a)

What are this graph’s vertices and edges?

\[V=\{0, 1, 2, 3, 4\}\]\[E=\{(0, 1), (1, 0), (0, 3), (1, 4), (3, 4)\}\]
(b)

Write out the adjacency matrix of this graph.

01234
0FTFTF
1TFFFT
2FFFFF
3FFFFT
4FFFFF
(c)

State the neighborhood of each vertex.

  • \(N_G(0)=\{1, 3\}\)
  • \(N_G(1)=\{0, 4\}\)
  • \(N_G(2)=\emptyset\)
  • \(N_G(3)=\{4\}\)
  • \(N_G(4)=\emptyset\)
(d)

Consider modeling movie actors as an undirected graph. The nodes represent actors and are labeled with the actor’s Screen Actors Guild number. There is an edge between every pair of actors if they appeared in at least one movie together. The weight of the edge is a non-negative integer that represents the number of movies they have appeared in together.

Which representation would make it more time-efficient to query how many movies two actors have appeared in together?
Check Answer
Exercise 21.2: Graph Operation Complexities
For each of the following graph operations, state the asymptotic runtime for both a graph in an adjacency list and adjacency matrix representation and identify whether a sparse or dense graph would be more performant.
(a)
Whether a certain edge is in the graph.
(b)
Visiting all edges in the graph.
Exercise 21.3: Exploring a Graph
You are given the following sets representing the vertices (\(V\)) and edges (\(E\)) in a directed graph: \[V=\{a, b, c, d, e\}\] \[E=\{(a, b), (a, c), (b, c), (b, d), (c, e), (d, e)\}\]
(a)
Draw the graph.
(b)
List all simple paths from \(a\) to \(e\). Which of these is the shortest?
(c)
Does this graph contain a cycle? If so, list the vertices in order.
Exercise 21.4: Validate Path
Suppose we add a method to the Graph interface that verifies if a list of edges forms a path on the graph. Implement this method for both representations of a graph.

Graph.java

1
2
/** Returns whether `edges` forms a path in this graph. */
boolean isPath(CS2110List<E> edges);
1
2
/** Returns whether `edges` forms a path in this graph. */
boolean isPath(CS2110List<E> edges);
Exercise 21.5: Simplify Path
A simple path does not revisit vertices. Given a path, convert it to a simple path with the same source and destination, i.e. by chopping off cycles. For instance, the simplified path of \(A \to B \to C \to D \to B \to E \) would be \(A \to B \to E \).
1
2
/** Returns a simplified version of `path` without any cycles. */
static <V> CS2110List<Edge<V>> simplifyPath(CS2110List<Edge<V>> path) { ... }
1
2
/** Returns a simplified version of `path` without any cycles. */
static <V> CS2110List<Edge<V>> simplifyPath(CS2110List<Edge<V>> path) { ... }
Exercise 21.6: Tracking Edge Count
Modify both graph implementations to maintain a field tracking their edge count, so it can be returned directly.
Exercise 21.7: Removing Vertices and Edges
Suppose we add the following methods to our interface Graph to support removal of edges and vertices from our graph.

Graph.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * Removes and returns the vertex labeled `label` and any incident edges from
 * this graph. Throws an IllegalArgumentException if `hasVertex(label) == false`.
 */
V removeVertex(String label);

/**
 * Removes and returns the edge between the vertices with the given labels.
 * Throws an IllegalArgumentException if `hasEdge(tailLabel, headLabel) == false`.
 */
E removeEdge(String tailLabel, String headLabel);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/**
 * Removes and returns the vertex labeled `label` and any incident edges from
 * this graph. Throws an IllegalArgumentException if `hasVertex(label) == false`.
 */
V removeVertex(String label);

/**
 * Removes and returns the edge between the vertices with the given labels.
 * Throws an IllegalArgumentException if `hasEdge(tailLabel, headLabel) == false`.
 */
E removeEdge(String tailLabel, String headLabel);
Implement these two methods for both representations of a graph.
Exercise 21.8: In-neighborhood
The in-neighborhood of vertex \(u \in V\) is the set of all vertices with an edge to \(u\). \[ \big\{ v \in V \colon (v,u) \in E \big\}. \] The in-degree is the size of this set, or the number of vertices that have an edge to \(u\). For both representations of a graph, implement the following operations defined in the Graph interface.
(a)
1
2
3
4
5
/**
 * Returns the in-degree of the vertex labeled `label`. 
 * Requires `hasVertex(label) == true`.
 */
int inDegree(String label);
1
2
3
4
5
/**
 * Returns the in-degree of the vertex labeled `label`. 
 * Requires `hasVertex(label) == true`.
 */
int inDegree(String label);
(b)
1
2
3
4
5
/**
 * Returns whether there is an edge with this vertex as its head and with a 
 * vertex labeled `tailLabel` as its tail.
 */
boolean hasInNeighbor(String tailLabel);
1
2
3
4
5
/**
 * Returns whether there is an edge with this vertex as its head and with a 
 * vertex labeled `tailLabel` as its tail.
 */
boolean hasInNeighbor(String tailLabel);
(c)
1
2
3
4
5
6
/**
 * Returns an object supporting iteration over all the edges connecting another 
 * vertex to this vertex in the graph. This vertex serves as the "head" 
 * vertex for each such edge.
 */
Iterable<E> incomingEdges();
1
2
3
4
5
6
/**
 * Returns an object supporting iteration over all the edges connecting another 
 * vertex to this vertex in the graph. This vertex serves as the "head" 
 * vertex for each such edge.
 */
Iterable<E> incomingEdges();
(d)
State the runtime complexities for each method. Compare these runtimes to those of methods related to out-neighborhoods. Why is there a discrepancy?
Exercise 21.9: Undirected Graph
Recall an undirected graph is a graph whose edges have no direction. If there is an edge from \(u \in V\) to \(v \in V\), you can travel both from \(u\) to \(v\) and \(v\) to \(u\). In an undirected graph, be aware that you will have to store each edge twice.
(a)
Implement an undirected graph represented as an adjacency list.
(b)
Implement an undirected graph represented as an adjacency matrix.
Exercise 21.10: Finding Paths
(a)

Write a method to determine whether there is a path of length 2 between two vertices.

Graph.java

1
2
3
4
5
6
/** 
 * Returns a length 2 path that starts at vertex labeled `start` and ends
 * at vertex labeled `dest`. Returns an empty list if no such path exists.
 * Requires `hasVertex(start) == true` and `hasVertex(dest) == true`.
 */
CS2110List<E> findLengthTwoPath(String start, String dest);
1
2
3
4
5
6
/** 
 * Returns a length 2 path that starts at vertex labeled `start` and ends
 * at vertex labeled `dest`. Returns an empty list if no such path exists.
 * Requires `hasVertex(start) == true` and `hasVertex(dest) == true`.
 */
CS2110List<E> findLengthTwoPath(String start, String dest);
(b)

Suppose we relax the constraint of the path being length 2. Write a recursive method that determines whether there is an \(n\)-edge “trail”, which can possibly have repeating edges, between two vertices.

Graph.java

1
2
3
4
5
6
/** 
 * Returns a length n path that starts at vertex labeled `start` and ends
 * at vertex labeled `dest`. Returns an empty list if no such path exists.
 * Requires `hasVertex(start) == true`, `hasVertex(dest) == true`, and `n >= 0`.
 */
CS2110List<E> findLengthNPath(int n, String start, String dest);
1
2
3
4
5
6
/** 
 * Returns a length n path that starts at vertex labeled `start` and ends
 * at vertex labeled `dest`. Returns an empty list if no such path exists.
 * Requires `hasVertex(start) == true`, `hasVertex(dest) == true`, and `n >= 0`.
 */
CS2110List<E> findLengthNPath(int n, String start, String dest);
Exercise 21.11: Finding Triangles
A triangle is a cycle that has length 3. Consider the following graph.
(a)
Identify the two triangles in this graph.
(b)

Implement the following method to determine whether a graph contains a cycle.

1
2
3
/** Returns whether `graph` has a cycle. */
static <V extends Vertex<?>, E extends Edge<V>> boolean 
  hasTriangle(Graph<V, E> graph) { ... }
1
2
3
/** Returns whether `graph` has a cycle. */
static <V extends Vertex<?>, E extends Edge<V>> boolean 
  hasTriangle(Graph<V, E> graph) { ... }