CHAPTER 10: The Graph ADT

© Ake13bk/Shutterstock

In Chapter 7 we looked at how a branching structure—the binary search tree—facilitates searching for data. In this chapter we see how another branching structure, the graph, is defined and implemented to support a variety of applications. Graphs consist of vertices and edges. The information held in a graph often is related to the structure of the graph itself, the relationships among the vertices and edges.

10.1 Introduction to Graphs

In Chapter 7 we studied trees. Trees provide a very useful way of representing relationships in which a hierarchy exists. That is, a node is pointed to by at most one other node (its parent), as pictured below.

If we remove the restriction that each node may have only one parent node, we have a data structure called a graph. A graph is made up of a set of nodes called vertices and a set of lines called edges (or arcs) that connect the vertices. The information held in a graph is related to the structure of the graph itself, the relationships among the vertices and edges. In this section we introduce the vocabulary of these relationships.

The set of edges describes relationships among the vertices. For instance, if the vertices are the names of cities, the edges that link the vertices could represent roads between pairs of cities. Because the road that runs between Houston and Austin also runs between Austin and Houston, the edges in this graph have no direction. This is called an undirected graph. If the edges that link the vertices represent flights from one city to another, however, the direction of each edge is important. The existence of a flight (edge) from Houston to Austin does not assure the existence of a flight (edge) from Austin to Houston. A graph whose edges are directed from one vertex to another is called a directed graph, or digraph.

From a programmer’s perspective, vertices represent whatever is the subject of our study: people, houses, cities, courses, and so on. Mathematically, however, vertices are the abstract concept upon which graph theory rests. In fact, a great deal of formal mathematics is associated with graphs. In other computing courses, you may analyze graphs and prove theorems about them. This text introduces the graph as an abstract data type, teaches some basic terminology, discusses how a graph might be implemented, and describes how algorithms that manipulate graphs make use of stacks, queues, and priority queues.

Formally, a graph G is defined as follows:

G = (V, E)

where

V(G) is a finite, nonempty set of vertices and E(G) is a set of edges (written as pairs of vertices).

The set of vertices is specified by listing them in set notation, within { } braces. The following set defines the six vertices of Graph1 pictured in Figure 10.1a:

V(Graph1) = {A, B, C, D, E, F}

Figure 10.1 Examples of graphs

The set of edges is specified by listing a sequence of edges. Each edge is denoted by writing the names of the two vertices it connects in parentheses, with a comma between them. For instance, the vertices in Graph1 are connected by the five edges described below:

E(Graph1) = {(A, B), (A, D), (B, C), (B, D), (E, F)}

Because Graph 1 is an undirected graph, the order of the vertices in each edge is unimportant. The set of edges in Graph 1 can also be described as follows:

E(Graph1) = {(B, A), (D, A), (C, B), (D, B), (F, E)}

If the graph is a digraph, the direction of the edge is indicated by which vertex is listed first. For instance, in Graph 2 pictured in Figure 10.1b, the edge (5, 7) represents a link from vertex 5 to vertex 7. There is no corresponding edge (7, 5) in Graph 2. In pictures of digraphs, the arrows indicate the direction of the relationship.

We do not have duplicate vertices or edges in a graph. This point is implied in the definition, because sets do not have repeated elements.

If two vertices in a graph are connected by an edge, they are said to be adjacent. In Graph 1 vertices A and B are adjacent, but vertices A and C are not. If the vertices are connected by a directed edge, then the first vertex is said to be adjacent to the second, and the second vertex is said to be adjacent from the first. For example, in Graph 2 vertex 5 is adjacent to vertices 7 and 9, while vertex 1 is adjacent from vertices 3 and 11.

The picture of Graph 3 in Figure 10.1c may look familiar; it is the tree we looked at in Chapter 9 in connection with the nonlinked representation of a binary tree. A tree is a special case of a directed graph in which each vertex may only be adjacent from one other vertex (its parent) and one vertex (the root) is not adjacent from any other vertex.

A path from one vertex to another consists of a sequence of vertices that connect them. For a path to exist, there must be an uninterrupted sequence of edges from the first vertex, through any number of vertices, to the second vertex. For example, in Graph 2, there is a path from vertex 5 to vertex 3, but not from vertex 3 to vertex 5. In a tree, such as Graph 3, there is a unique path from the root to every other vertex in the tree.

A cycle is a path that begins and ends at the same vertex. For example, in Graph 1 the path A-B-D-A is a cycle.

In an undirected graph we say that two vertices are connected (connected vertices) if there is a path between them. Note that in Graph 1, vertex A is connected to vertices B, C, and D but not to E or F. A connected component is a maximal set of vertices that are connected to each other, along with the edges associated with those vertices. Graph 1 consists of two connected components {A, B, C, D} and {E, F}. Each vertex of an undirected graph belongs to one connected component. So does each edge.

An undirected graph is a connected graph when it consists of a single connected component. Otherwise it is a disconnected graph.

A complete graph is one in which every vertex is adjacent to every other vertex. Figure 10.2 shows two complete graphs. If there are N vertices, there are N * (N – 1) edges in a complete directed graph and N * (N – 1) / 2 edges in a complete undirected graph.

Figure 10.2 Two complete graphs

A weighted graph is a graph in which each edge carries a value. Weighted graphs can be used to represent applications in which the value of the connection between the vertices is important, not just the existence of a connection. For instance, in the weighted graph pictured in Figure 10.3, the vertices represent cities and the edges indicate the Air Busters Airlines flights that connect the cities. The weights attached to the edges represent the air distances between pairs of cities.

To see whether we can get from Denver to Washington, we look for a path between them. If the total travel distance is determined by the sum of the distances between each pair of cities along the way, we can calculate the travel distance by adding the weights attached to the edges that constitute the path between them. There may be multiple paths between two vertices. Later in this chapter, we talk about a way to find the shortest path between two vertices (Figure 10.4).

Figure 10.3 A weighted graph

Figure 10.4 Graph terminology

10.2 The Graph Interface

We described a graph at the abstract level as a set of vertices and a set of edges that connect some or all of the vertices to one another. What kind of operations are defined on a graph? In this chapter we specify and implement a small set of useful graph operations for a directed, weighted graph. Many other operations on graphs can be defined as well. We have chosen operations that are useful when building applications to answer typical questions one might ask about graphs. For example:

  • Does a path exist between vertex A and vertex D? Can we fly from Atlanta to Detroit?

  • What is the total weight along this path from A to D? How much does it cost to fly from Atlanta to Detroit? What is the total distance?

  • What is the shortest path from A to D? What is the cheapest way to get from Atlanta to Detroit?

  • If I start at vertex A, where can I go? What cities are accessible if I start in Atlanta?

  • How many connected components are in the graph? What groups of cities are connected to each other?

Note that we do not implement operations that directly answer any of the above questions within our Graph ADT. Instead we provide more primitive operations that can be used, in combination with each other, to answer these questions and more. In Section 10.4, “Application: Graph Traversals,” we show how this is accomplished for a few of the above questions. Others are left as exercises.

Our specification for the Graph ADT, found in the WeightedGraphInterface listing below, includes 11 public methods. As expected, it includes methods to check whether the graph is empty or full, and methods to add vertices and edges. Here we describe the remaining, more unusual methods.

The hasVertex method can be used to check whether the argument object is used as a vertex in the graph. This issue is important because many of the other methods that accept a vertex as an argument assume, as a precondition, that the given vertex exists within the graph. Equivalence of vertices is determined using the vertices’ equals method—so if the vertex class has overwritten the Object class equals method, then equality will be as defined within the vertex class; otherwise, it is defined using the default approach of comparing references.

The weightIs method returns the weight of the edge between two given vertices; if there is no such edge, it returns a special value indicating that fact. The special value could vary from one application to another. For example, the value -1 could be used for a graph whose edges represent distances, because there is no such thing as a negative distance. The special value for a specific application could be passed to the Graph constructor, if that capability is provided by the class that implements the interface. The weightIs method lets us determine whether a given edge is in the graph, which is useful, for example, to see if two vertices are connected. Of course, if two vertices are connected it is used to determine the weight of the edge between them, useful for answering questions about the total weight of a path between two vertices or the shortest path in terms of weight.

A very interesting method is getToVertices, which returns a queue of vertex objects. The idea is that an application may need to know which vertices a given vertex is adjacent to in order to determine what paths exist through the graph. This method returns the collection of such vertices as a queue. The application can then dequeue the vertices one at a time, as needed. The return value of the method is of “type” QueueInterface. The implementation programmer can choose to use any queue class that implements this interface.

Another approach to solving the “get to vertices” problem is to have the method return a Java Iterator object. We have used iterators with many of our previous collection ADTs, but in this case we will use the queue.

To answer many of the questions we listed above, an application must be able to traverse the graph—that is, it must visit the vertices of the graph and perform some operation at each vertex. Because so many paths through a graph are possible, it is not unusual for the traversal algorithm to attempt to visit a vertex more than once. In such cases it is important for the application to “know” that it has previously visited the vertex. To facilitate this, our interface includes several methods related to marking vertices as visited. The markVertex and isMarked methods are used to mark vertices and check for marks, respectively. The clearMarks method clears all of the marks throughout the graph; it is used to prepare for a new traversal. Finally, the getUnmarked method returns an unmarked vertex. This ability is useful for beginning a traversal or for continuing a traversal, when the application is not sure it has visited every vertex. The ability to mark vertices is used to answer questions about shortest paths and the number of connected components.

Below is the WeightedGraphInterface. It is found in the ch10.graphs package.

//---------------------------------------------------------------------------
// WeightedGraphInterface.java       by Dale/Joyce/Weems           Chapter 10
//
// Interface for classes that implement a directed graph with weighted edges.
// Vertices are objects of class T and can be marked as having been visited.
// Edge weights are integers.
// Equivalence of vertices is determined by the vertices' equals method.
//
// General precondition: Except for the addVertex and hasVertex methods,
// any vertex passed as an argument to a method is in this graph.
//---------------------------------------------------------------------------

package ch10.graphs;

import ch04.queues.*;

public interface WeightedGraphInterface<T>
{
  boolean isEmpty();
  // Returns true if this graph is empty; otherwise, returns false.

  boolean isFull();
  // Returns true if this graph is full; otherwise, returns false.

  void addVertex(T vertex);
  // Preconditions:   This graph is not full.
  //                  vertex is not already in this graph.
  //                  vertex is not null.
  //
  // Adds vertex to this graph.

  boolean hasVertex(T vertex);
  // Returns true if this graph contains vertex; otherwise, returns false.

  void addEdge(T fromVertex, T toVertex, int weight);
  // Adds an edge with the specified weight from fromVertex to toVertex.

  int weightIs(T fromVertex, T toVertex);
  // If edge from fromVertex to toVertex exists, returns the weight of edge;
  // otherwise, returns a special "null-edge" value.

  QueueInterface<T> getToVertices(T vertex);
  // Returns a queue of the vertices that vertex is adjacent to.

  void clearMarks();
  // Unmarks all vertices.

  void markVertex(T vertex);
  // Marks vertex.

  boolean isMarked(T vertex);
  // Returns true if vertex is marked; otherwise, returns false.

  T getUnmarked();
  // Returns an unmarked vertex if any exist; otherwise, returns null.
}

10.3 Implementations of Graphs

Array-Based Implementation

A simple way to represent V(graph), the vertices in the graph, is to use an array where the array elements are the vertices. For example, if the vertices represent city names, the array might hold strings. A simple way to represent E(graph), the edges in a graph, is to use an adjacency matrix, a two-dimensional array of edge values (weights), where the indices of a weight correspond to the vertices connected by the edge. Thus, a graph consists of an integer variable numVertices, a one-dimensional array vertices, and a two-dimensional array edges. Figure 10.5 depicts the implementation of the graph of Air Busters’ flights among the seven cities shown in Figure 10.3. For simplicity, we omit the additional boolean data needed to mark vertices as “visited” during a traversal from the figure. Although the city names in Figure 10.5 are in alphabetical order, there is no requirement that the elements in this array be sorted.

Figure 10.5 Matrix representation of graph of flight connections between cities

At any time, within this representation of a graph,

  • numVertices is the number of vertices in the graph.

  • V(graph) is contained in vertices[0] to vertices [numVertices - 1].

  • E(graph) is contained in the square matrix edges[0][0] to edges[numVertices - 1][numVertices 1].

The names of the cities are contained in vertices. The weight of each edge in edges represents the air distance between two cities that are connected by a flight. For example, the value in edges[1][3] tells us that there is a direct flight between Austin and Dallas, and that the air distance is 200 miles. A NULL_EDGE value (0) in edges[1][6] tells us that the airline has no direct flights between Austin and Washington. Because this is a weighted graph in which the weights are air distances, we use int for the edge value type. If it were not a weighted graph, the edge value type could be boolean, with each position in the adjacency matrix being true if an edge exists between the pair of vertices, and false if no edge exists.

Here is the beginning of the definition of class WeightedGraph. We assume that the edge value type is int and that a null edge is indicated by a 0 value.

//---------------------------------------------------------------------------
// WeightedGraph.java            by Dale/Joyce/Weems               Chapter 10
//
// Implements a directed graph with weighted edges.
// Vertices are objects of class T and can be marked as having been visited.
// Edge weights are integers.
// Equivalence of vertices is determined by the vertices' equals method.
//
// General precondition: Except for the addVertex and hasVertex methods,
// any vertex passed as an argument to a method is in this graph.
//---------------------------------------------------------------------------

package ch10.graphs;

import ch04.queues.*;

public class WeightedGraph<T> implements WeightedGraphInterface<T>
{
  public static final int NULL_EDGE = 0;
  private static final int DEFCAP = 50;  // default capacity
  private int numVertices;
  private int maxVertices;
  private T[] vertices;
  private int[][] edges;
  private boolean[] marks;  // marks[i] is mark for vertices[i]

  public WeightedGraph()
  // Instantiates a graph with capacity DEFCAP vertices.
  {
    numVertices = 0;
    maxVertices = DEFCAP;
    vertices = (T[]) new Object[DEFCAP];1
    marks = new boolean[DEFCAP];
    edges = new int[DEFCAP][DEFCAP];
  }

  public WeightedGraph(int maxV)
  // Instantiates a graph with capacity maxV.
  {
    numVertices = 0;
    maxVertices = maxV;
    vertices = (T[]) new Object[maxV];1
    marks = new boolean[maxV];
    edges = new int[maxV][maxV];
  }

The class constructors have to allocate the space for vertices, marks (the boolean array indicating whether a vertex has been marked), and edges. The default constructor sets up space for a graph with DEFCAP (50) vertices. The parameterized constructor lets the user specify the maximum number of vertices.

The addVertex operation puts a vertex into the next free space in the array of vertices and increments the number of vertices. Because the new vertex has no edges defined yet, it also initializes the appropriate row and column of edges to contain NULL_EDGE (0 in this case).

public void addVertex(T vertex)
// Preconditions:   This graph is not full.
//                  vertex is not already in this graph.
//                  vertex is not null.
//
// Adds vertex to this graph.
{
  vertices[numVertices] = vertex;
  for (int index = 0; index < numVertices; index++)
  {
     edges[numVertices][index] = NULL_EDGE;
     edges[index][numVertices] = NULL_EDGE;
  }
  numVertices++;
}

To add an edge to the graph, we must first locate the fromVertex and toVertex that define the edge being added. These values become the arguments to addEdge and are of the generic T class. Of course, the client really passes references to the vertex objects, because that is how we manipulate objects in Java. We are implementing our graphs “by reference” so this strategy should not pose a problem for the client. To index the correct matrix slot, we need the index in the vertices array that corresponds to each vertex. Once we know the indices, it is a simple matter to set the weight of the edge in the matrix.

To find the index of each vertex, let us write a private search method that receives a vertex and returns its location (index) in vertices. Based on the general precondition stated in the opening comment of the WeightedGraph class, we assume that the fromVertex and toVertex arguments passed to addEdge are already in V(graph). This assumption simplifies the search method, which we code as helper method indexIs. Here is the code for indexIs and addEdge:

private int indexIs(T vertex)
// Returns the index of vertex in vertices.
{
  int index = 0;
  while (!vertex.equals(vertices[index]))
    index++;
  return index;
}

public void addEdge(T fromVertex, T toVertex, int weight)
// Adds an edge with the specified weight from fromVertex to toVertex.
{
  int row;
  int column;

  row = indexIs(fromVertex);
  column = indexIs(toVertex);
  edges[row][column] = weight;
}

The weightIs operation is the mirror image of addEdge.

public int weightIs(T fromVertex, T toVertex)
// If edge from fromVertex to toVertex exists, returns the weight of edge;
// otherwise, returns a special "null-edge" value.
{
  int row;
  int column;

  row = indexIs(fromVertex);
  column = indexIs(toVertex);
  return edges[row][column];
}

The last graph operation that we address is getToVertices. This method receives a vertex as an argument, and it returns a queue of vertices that the vertex is adjacent to (or, another way of looking at it, that are adjacent from the designated vertex). That is, it returns a queue of all the vertices that we can get to from this vertex in one step. Using an adjacency matrix to represent the edges, it is a simple matter to determine the vertices to which the vertex is adjacent. We merely loop through the appropriate row in edges; whenever a value is found that is not NULL_EDGE, we add the corresponding vertex to the queue.

public QueueInterface<T> getToVertices(T vertex)
// Returns a queue of the vertices that are adjacent from vertex.
{
  QueueInterface<T> adjVertices = new LinkedQueue<T>();
  int fromIndex, toIndex;
  fromIndex = indexIs(vertex);
  for (toIndex = 0; toIndex < numVertices; toIndex++)
    if (edges[fromIndex][toIndex] != NULL_EDGE)
      adjVertices.enqueue(vertices[toIndex]);
  return adjVertices;
}

We leave writing isFull, isEmpty, hasVertex, and the marking operations (clearMarks, markVertex, isMarked, and getUnmarked) for you as exercises.

Linked Implementation

The advantages of representing the edges in a graph with an adjacency matrix are twofold: speed and simplicity. Given the indices of two vertices, determining the existence (or the weight) of an edge between them is an O(1) operation. The problem with adjacency matrices is that their use of space to store the edge information is O(N2), where N is the maximum number of vertices in the graph. If the maximum number of vertices is large but the number of actual vertices is small, or if a graph is sparse (the ratio between the number of edges and number of vertices is small), adjacency matrices waste a lot of space. We can save space by allocating memory as needed at run time, using linked structures. Adjacency lists are linked lists, one list per vertex, that identify the vertices to which each vertex is connected. They can be implemented in several different ways. Figures 10.6 and 10.7 show two different adjacency list representations of the graph in Figure 10.3.

In Figure 10.6, the vertices are stored in an array. Each component of this array contains a reference to a linked list of edge information. Each item in these linked lists contains an index number, a weight, and a reference to the next item in the adjacency list. Look at the adjacency list for Denver. The first item in the list indicates that there is a 1,400-mile flight from Denver to Atlanta (the vertex whose index is 0) and the second item indicates a 1,000-mile flight from Denver to Chicago (the vertex whose index is 2).

No arrays are used in the implementation illustrated in Figure 10.7. Instead, the list of vertices is also implemented as a linked list. Now each item in the adjacency list contains a reference to the vertex information rather than the index of the vertex. Because Figure 10.7 includes so many of these references, we have used text to describe the vertex that each reference designates rather than draw them as arrows.

We leave the implementation of the Graph class methods using the linked approaches as a programming exercise.

Figure 10.6 Adjacency list representation of graphs

10.4 Application: Graph Traversals

The graph specification given in Section 10.2, “The Graph Interface,” includes only the most basic operations; it does not include any traversal operations. As you might imagine, we can traverse a graph in many different orders. Therefore, we treat traversal as a graph application rather than as an innate operation. The basic operations given in our specification allow us to implement different traversals independent of how the graph itself is implemented.

In Section 7.1, “Trees,” we discussed two ways of traversing a general tree, depth-first, that repeatedly goes as deep as it can in the tree and then backs up, and breadth-first, that fans out across the tree level by level. With graphs, both depth-first and breadth-first strategies are also extremely important. We discuss algorithms for employing both strategies within the context of determining whether two cities are connected in our airline example. Note that these same basic approaches can be used to solve many other problems, when traversing the vertices of a graph in an organized fashion is required.

Figure 10.7 Alternate adjacency list representation of graphs

Depth-First Searching

One question we can answer using the graph in Figure 10.3 is “Can I get from city X to city Y on my favorite airline?” This is equivalent to asking, “Does a path exist in the graph from vertex X to vertex Y?” Using a depth-first strategy, we develop an algorithm IsPathDF that determines whether a path exists from startVertex to endVertex.

We need a systematic way to keep track of the cities as we investigate them. With a depth-first search, we examine the first vertex that is adjacent from startVertex; if it is endVertex, the search is over. Otherwise, we examine all of the vertices that can be reached in one step (are adjacent) from this first vertex. While we examine these vertices, we need to store the remaining vertices adjacent from startVertex that have not yet been examined. If a path does not exist from the first vertex, we come back and try the second vertex, third vertex, and so on. Because we want to travel as far as we can down one path, backtracking if the endVertex is not found, a stack is a good structure for storing the vertices.

However, there is a problem with the approach we just described. Graphs can contain cycles. We can fly from Washington to Dallas to Denver and then back to Washington. Do you see why this is an issue in the approach described above? Not only is it likely that we will unnecessarily repeat the processing of a city (a vertex), it is even possible to get caught in an infinite loop (e.g., when processing Washington we push Atlanta onto the stack, and when processing Atlanta we push Washington onto the stack). We did not have this issue when designing our tree traversals in Chapter 7 because trees by definition do not have cycles.

How can we fix this? You probably recall that addressing this problem was the reason we included the ability to mark vertices within our Graph ADT. Our solution is to mark vertices to indicate that they have been placed on the stack. And, of course, to use those marks to prevent pushing a vertex onto the stack more than once. This way we eliminate repetitiously processing a vertex and the possibility of an infinite loop. Here is the algorithm:

Let us apply this algorithm to the sample airline-route graph in Figure 10.3. We will trace the algorithm applied to the case where Austin is the starting city and Washington is the desired destination. In Figure 10.8 we repeat the airport graph, abbreviating the names of the cities using their first two letters and dropping the distances, because they are not pertinent to our current question. We use boldface to indicate that a city has been processed—that it has been checked to see if it is the destination and if not then its adjacent cities have been placed on the stack if appropriate. We also sequentially number the entry edge into a city when it is processed, the edge that is followed to lead to the processing of the city. Finally, we indicate that a city has been marked by placing a checkmark beside it.

Figure 10.8 IsPathDF algorithm trace

Okay, so we want to fly from Austin to Washington. We initialize our search by marking our starting city and pushing it onto the stack (Figure 10.8a). At the beginning of the do-while loop we retrieve the current city, Austin, from the stack using the top method and then remove it from the stack using the pop method. Because Austin is not our final destination we examine the places we can reach directly from it—Dallas and Houston (we will process adjacent vertices in alphabetical order); neither of these is marked so we mark them and push them onto the stack (Figure 10.8b). That completes the processing of Austin.

At the beginning of the second iteration we retrieve and remove the top vertex from the stack—Houston. Houston is not our destination, so we resume our search from there. There is only one flight out of Houston, to Atlanta; Atlanta has not yet been marked so we mark it and push it onto the stack (Figure 10.8c), finishing our processing of Houston. Again we retrieve and remove the top vertex from the stack. Atlanta is not our destination, so we continue searching from there. Atlanta has flights to two cities: Houston and Washington. Houston was already marked so at this stage we mark only Washington and push it onto the stack (Figure 10.8d).

Finally, we retrieve and remove the top vertex from the stack, Washington. This is our destination, so the search is complete (Figure 10.8e).

As indicated by the final part of Figure 10.8e we had a very successful and efficient search, processing only four cities during our search including the source and destination cities. Although the algorithm (since it is correct!) guarantees success in that it will always correctly identify whether a path exists, it might not always be so efficient. Consider a new question: can we fly from Dallas to Austin. A quick check of the graph tells us that yes we can, in fact it is a one hop trip. Yet in this case the algorithm will process every vertex in the graph before determining that the path exists. The reader is encouraged to verify this for themselves by tracing the algorithm on the new problem.

Method isPathDF receives a graph object, a starting vertex, and a target vertex. It uses the depth-first algorithm described above to determine whether a path exists from the starting city to the ending city, displaying the names of all cities processed in the search. Nothing in the method depends on the implementation of the graph. The method is implemented as a graph application; it uses the Graph ADT operations without knowing how the graph is represented. We use a graph of strings, since we can represent a city by its name. In the following code, we assume that a stack and a queue implementation have been imported into the client class. (The isPathDF method is included in the UseGraph.java application, available in the ch10.apps package.)

private static boolean isPathDF(WeightedGraphInterface<String> graph,
                                String startVertex, String endVertex)
// Returns true if a path exists on graph, from startVertex to endVertex;
// otherwise returns false. Uses depth-first search algorithm.
{
  StackInterface<String> stack = new LinkedStack<String>();
  QueueInterface<String> vertexQueue = new LinkedQueue<String>();

  boolean found = false;
  String currVertex;       // vertex being processed
  String adjVertex;        // adjacent to currVertex

  graph.clearMarks();
  graph.markVertex(startVertex);
  stack.push(startVertex);

  do
  {
    currVertex = stack.top();
    stack.pop();
    System.out.println(currVertex);
    if (currVertex.equals(endVertex))
       found = true;
    else
    {
      vertexQueue = graph.getToVertices(currVertex);
      while (!vertexQueue.isEmpty())
      {
        adjVertex = vertexQueue.dequeue();
        if (!graph.isMarked(adjVertex))
        {
          graph.markVertex(adjVertex);
          stack.push(adjVertex);
        }
      }
    }
  } while (!stack.isEmpty() && !found);
  return found;
}

Breadth-First Searching

A breadth-first search looks at all possible paths at the same depth before it goes to a deeper level. In our flight example, a breadth-first search checks all possible one-stop connections before checking any two-stop connections. For most travelers, this strategy is the preferred approach for booking flights.

When we come to a dead end in a depth-first search, we back up as little as possible. We then try another route from a recent vertex—the route on top of our stack. In a breadth-first search, we want to back up as far as possible to find a route originating from the earliest vertices. The stack is not the right structure for finding an early route, because it keeps track of things in the order opposite of their occurrence—the latest route is on top. To keep track of things in the order in which they happened, we use a FIFO queue. The route at the front of this queue is a route from an earlier vertex; the route at the back of the queue is from a later vertex. To modify the search to use a breadth-first strategy, we change all calls to stack operations to the analogous FIFO queue operations in the previous algorithm. Just as we did with the depth-first search, we mark a vertex before placing it in the queue, to avoid repetitive processing. Here is the breadth-first algorithm:

Figure 10.9 shows the sequence in which vertices are visited for several depth-first and breadth-first searches using our flights example. The numbers in the figure indicate the order in which entry edges are used to “visit” vertices. Keep in mind our assumption that vertices are placed into the queue (or stack) in alphabetical order.

Let us revisit our previous example, searching for a path from Austin to Washington, but this time with a breadth-first search. We start with Austin in the queue. We dequeue Austin and enqueue all the cities that can be reached directly from it: Dallas and Houston. Then we dequeue the front queue element, Dallas (indicated by the circled 1 in Figure 10.9b). Because Dallas is not the destination we seek, we enqueue all the adjacent cities from Dallas that have not yet been marked: Chicago and Denver. (Austin has been visited already, so it is not enqueued.) Again we dequeue the front element from the queue. This element is the other “one-stop” city: Houston (indicated by the circled 2 in Figure 10.9b). Houston is not the desired destination, so we continue the search. There is only one flight out of Houston, and it is to Atlanta. Because we have not marked Atlanta before, it is enqueued. This processing continues until Washington is put into the queue (from Atlanta), and is finally dequeued. We have found the desired city, and the search is complete.

Figure 10.9 Examples of search paths

As you can see in Figure 10.9b the breadth-first search of a path from Austin to Washington processed every city in the graph before determining that the path existed. It was not as efficient as the depth-first search for the same path (Figure 10.9a). On the other hand, a comparison of Figures 10.9c and 10.9d shows that in another case, searching for a path from Dallas to Austin, the breadth-first search is more efficient. It is really just a matter of luck— the number of steps depends on the graph configuration and the source and destination vertices. Comparing Figures 10.9e and 10.9f shows an example, searching for a path from Washington to Austin, where the same number of entry edges are used in each approach.

The source code for the breadth-first search approach is identical to the depth-first search code, except for the replacement of the stack with a FIFO queue. It is also included in the UseGraph.java application, as the method isPathBF, available in the ch10.apps package.

10.5 Application: The Single-Source Shortest-Paths Problem

We know from the two search operations discussed in Section 10.4, “Application: Graph Traversals,” that there may be multiple paths from one vertex to another. Continuing the example scenario from that section, suppose that we want to find the shortest path from Austin to each of the other cities that Air Busters serves. By “shortest path” we mean the path whose edge values (weights), added together, have the smallest sum. Consider the following two paths from Austin to Washington:

Clearly, the first path is preferable, unless we want to collect extra frequent-flyer miles.

Let us develop an algorithm that displays the shortest path from a designated starting city to every other city in the graph. As in the two graph search algorithms described earlier, we need an auxiliary structure for storing cities that we process later. By retrieving the city that was most recently put into the structure, the depth-first search tries to keep going “forward.” It tries a one-flight solution, then a two-flight solution, then a three-flight solution, and so on. It backtracks to a fewer-flight solution only when it reaches a dead end. That approach is not suitable for our shortest-paths problem.

By retrieving the city that had been in the structure the longest time, the breadth-first search tries all one-flight solutions, then all two-flight solutions, and so on. The breadth-first search finds a path with a minimum number of flights. But a minimum number of flights does not necessarily mean the minimum total distance. That approach is also unsuitable for our shortest-paths problem.

Unlike the depth-first and breadth-first searches, this shortest-path traversal must use the number of miles (edge weights) between cities. We want to retrieve the vertex that is closest to the current vertex—that is, the vertex connected with the minimum edge weight. By always adding the next shortest path we are guaranteed to identify the correct paths. If we consider minimum distance to be the highest priority, then we know the perfect structure—the priority queue. Our algorithm can use a priority queue whose elements are flights (edges) with the distance from the starting city as the priority. That is, the elements in the priority queue are objects with three attributes: fromVertex, toVertex, and distance. We use a class named Flight to define these objects. The class implements the Comparable<Flight> interface, using the distance attribute to compare two flights (shorter is better). It provides a constructor that accepts three arguments, one for each attribute, and it provides the standard setter and getter methods for the attributes. The code is found in our support package. Here is the shortest-path algorithm:

The algorithm for the shortest-path traversal is similar to those we used for the depth-first and breadth-first searches, albeit with three major differences:

  1. We use a priority queue rather than a FIFO queue or stack. Note that when processing halts there can still be many flights left on the priority queue, but they are longer flights than the ones that have been identified as being part of the solution.

  2. We stop only when there are no more cities to process; there is no destination.

  3. It is incorrect if we use a store-by-reference priority queue!

When we code this algorithm, we are likely to make a subtle, but crucial error. This error is related to the fact that our queues store information “by reference” and not “by copy.” Take a minute to review the algorithm to see if you can spot the error before continuing.

Recall the feature section in Chapter 5 that discussed the dangers of storing information by reference. In particular, it warned us to be careful when inserting an object into a structure and later making changes to that object. If we use the same reference to the object when we make changes to it, the changes are made to the object that is in the structure. Sometimes this outcome is what we want (see the word frequency counter application in Chapter 7); at other times it causes problems, as in the current example. Here is the incorrect part of the algorithm:

while more vertices in vertexQueue
  Get next vertex from vertexQueue
  if vertex not marked
      flight.setToVertex(vertex)
      flight.setDistance(minDistance + graph.weightIs(flight.getFromVertex(), vertex))
      pq.enqueue(flight)

Now can you see the problem? This part of the algorithm walks through the queue of vertices adjacent to the current vertex and enqueues Flight objects onto the priority queue pq based on the information discovered there. The flight variable is actually a reference to a Flight object. Suppose the queue of adjacent vertices holds information related to the cities Atlanta and Houston. The first time through this loop, we insert information related to Atlanta in flight and enqueue it in pq. The next time through the loop, however, we make changes to the Flight object referenced by flight. We update it to contain information about Houston using the setter methods; and again we enqueue it in pq. So now pq contains information about Atlanta and Houston, correct? Nope. When we change the information in flight to the Houston information, those changes are reflected in the flight that is already on pq. The flight variable still references that object. In reality, the pq structure now contains two references to the same flight, and that flight contains Houston information.

To solve this problem, we must create a new flight object before storing on pq. Here is the corrected version of that part of the algorithm:

while more vertices in vertexQueue
  Get next vertex from vertexQueue
  if vertex not marked
     Set newDistance to minDistance + graph.weightIs(flight.getFromVertex(), vertex)
     Create new Flight(flight.getFromVertex(), vertex, newDistance)
     pq.enqueue(newFlight)

Here is the source code for the shortest-path algorithm (also included in the UseGraph. java application). As before, the code assumes that a priority queue and a queue implementation have been imported into the client class. For the priority queue, we use our HeapPriQ class from Chapter 9. We want a smaller distance to indicate a higher priority, but our HeapPriQ class implements a maximum heap, returning the largest value from the dequeue method. To fix this problem, we could define a new heap class, a minimum heap. But there is an easier way. The current HeapPriQ class bases its decision about what is “larger” on the values returned by the flight’s compareTo method. Thus, we just define the compareTo method of the Flight class to indicate that the current flight is “larger” than the argument flight if its distance is smaller. For every flight in the heap’s tree, flight.distance is then less than or equal to the distance value of each of its children. We can still use our maximum heap.

private static void shortestPaths(WeightedGraphInterface<String> graph,
                                  String startVertex)
// Writes the shortest distance from startVertex to every
// other reachable vertex in graph.
{
  Flight flight;
  Flight saveFlight;         // for saving on priority queue
  int minDistance;
  int newDistance;

  PriQueueInterface<Flight> pq = new HeapPriQ<Flight>(20);
  String vertex;
  QueueInterface<String> vertexQueue = new LinkedQueue<String>();

  graph.clearMarks();
  saveFlight = new Flight(startVertex, startVertex, 0);
  pq.enqueue(saveFlight);

  System.out.println("Last Vertex Destination Distance");
  System.out.println("------------------------------------");

  do
  {
    flight = pq.dequeue();
    if (!graph.isMarked(flight.getToVertex()))
    {
      graph.markVertex(flight.getToVertex());
      System.out.println(flight);
      flight.setFromVertex(flight.getToVertex());
      minDistance = flight.getDistance();
      vertexQueue = graph.getToVertices(flight.getFromVertex());
      while (!vertexQueue.isEmpty())
      {
        vertex = vertexQueue.dequeue();
        if (!graph.isMarked(vertex))
        {
          newDistance = minDistance
                        + graph.weightIs(flight.getFromVertex(), vertex);
          saveFlight = new Flight(flight.getFromVertex(), vertex,
                                  newDistance);
          pq.enqueue(saveFlight);
        }
      }
    }
  } while (!pq.isEmpty());
  System.out.println();
}

The output from this method is a table of city pairs (edges) showing the total minimum distance from startVertex to each of the other vertices in the graph, as well as the last vertex visited before the destination. We assume that printing a vertex means printing the name of the corresponding city. If graph contains the information shown in Figure 10.3, the method called

shortestPaths(graph, startVertex);

where startVertex corresponds to Washington, would print the following:

Last Vertex Destination Distance
Washington Washington 0
Washington Atlanta 600
Washington Dallas 1,300
Atlanta Houston 1,400
Dallas Austin 1,500
Dallas Denver 2,080
Dallas Chicago 2,200

 

The shortest-path distance from Washington to each destination is shown in the two columns to the right. For example, our flights from Washington to Chicago total 2,200 miles. The left-hand column shows which city immediately preceded the destination in the traversal. We want to figure out the shortest path from Washington to Chicago. First we find our destination, Chicago, in the Destination (middle) column, and see from the left-hand column that the next-to-last vertex in the path is Dallas. Now we look up Dallas in the Destination (middle) column—the vertex before Dallas is Washington. The whole path is Washington–Dallas–Chicago. (We might want to consider another airline for a more direct route!)

Unreachable Vertices

You may have noticed that in all of our examples so far we have been able to “reach” all of the other vertices in our graphs from our given starting vertex. What if this is not the case? Consider the weighted graph in Figure 10.10, which depicts a new set of airline-flight legs. It is identical to Figure 10.3 except that we removed the Washington–Dallas leg. If we invoke our shortestPaths method, passing it this graph and a starting vertex of Washington, we get the following output:

Last Vertex Destination Distance
Washington Washington 0
Washington Atlanta 600
Atlanta Houston 1,400

 

A careful study of the new graph confirms that one can reach Atlanta and Houston only, when starting in Washington, at least when flying Air Buster Airlines.

Suppose we extend the specification of our shortestPaths method to require that it also prints the unreachable vertices. How can we determine the unreachable vertices after we have generated the information about the reachable ones? Easy! The unreachable vertices are the unmarked vertices. We simply need to check whether any vertices remain unmarked. Our Graph ADT provides the operation getUnmarked specifically for this situation. So we simply add the following code to our method:

Figure 10.10 A new set of airline-flight legs


System.out.println("The unreachable vertices are:");
vertex = graph.getUnmarked();
while (vertex != null)
{
  System.out.println(vertex);
  graph.markVertex(vertex);
  vertex = graph.getUnmarked();
}

Now the output from the call to shortestPaths would be

Last Vertex Destination Distance
Washington Washington 0
Washington Atlanta 600
Atlanta Houston 1,400

 

The unreachable vertices are:

Austin
Chicago
Dallas
Denver

In Exercise 43 we ask you to investigate counting the “connected components” of a graph. This is another interesting application of the getUnmarked method.

Summary

In this chapter, we discussed graphs. After reviewing graph terminology we defined a Graph ADT and discussed implementation approaches, providing a partially finished array-based implementation. The set of abstract methods included in WeightedGraphInterface is designed to allow an application to implement graph-related algorithms. Three such algorithms, depth-first path discovery, breadth-first path discovery, and shortest paths, were presented.

The array-based graph implementation provides time-efficient operations. We also discussed a space-efficient reference-based implementation. Time and space efficiency trade-offs are often the key considerations when choosing among alternative implementations of a data structure.

Graphs are the most complex structure we have studied. They are very versatile and are a good way to model many real-world objects and situations. Because many different types of applications could potentially use graphs, numerous variations and generalizations of their definitions and implementations exist. In addition, many advanced algorithms for manipulating and traversing graphs have been discovered. They are generally covered in detail in advanced computer science courses on algorithms.

Exercises

10.1 Introduction to Graphs

  1. True/False. Explain your answer—in some cases drawing an example figure suffices for an explanation.

    1. A graph can have more vertices than edges.

    2. A graph can have more edges than vertices.

    3. A graph with just one vertex is connected.

    4. An edgeless graph with two or more vertices is disconnected.

    5. A graph with three vertices and one edge is disconnected.

    6. A connected graph with N vertices must have at least N–1 edges.

    7. A graph with five vertices and four edges is connected.

    8. An edge cannot connect a vertex to itself.

    9. All graphs are trees.

    10. All trees are graphs.

    11. A directed graph must be weighted.

  2. Each of the following can be modeled as graphs. Describe in each case how you would define the vertices and how you would define the edges.

    1. Friendships

    2. Television show costars

    3. Research literature citations

    4. The Internet

    5. The Web

  3. For each example in Exercise 2, describe how the graph could be weighted; that is, what might the weights on the edges of the graph represent?

  4. Draw an undirected graph, if possible (if not possible explain why not) that has

    1. five vertices and three edges and is connected.

    2. five vertices, three edges, and two connected components.

    3. five vertices, two edges, and three connected components.

    4. five vertices, two edges, and two connected components.

    5. five vertices, six edges, and two connected components.

    Use the following description of an undirected graph in Exercises 5 and 6:

    EmployeeGraph = (V, E)
    V(EmployeeGraph) = {Susan, Darlene, Mike, Fred, John, Sander, Lance, Jean, Brent, Fran}
    E(EmployeeGraph) = {(Susan, Darlene), (Fred, Brent), (Sander, Susan), (Lance, Fran), (Sander, Fran), (Fran, John), (Lance, Jean), (Jean, Susan), (Mike, Darlene), (Brent, Lance), (Susan, John)}
  5. Draw a picture of EmployeeGraph.

  6. Which one of the following phrases best describes the relationship represented by the edges between the vertices in EmployeeGraph?

    1. “works for”

    2. “is the supervisor of”

    3. “is senior to”

    4. “works with”

    Use the following specification of a directed graph in Exercises 79:

    ZooGraph = (V, E)
    V(ZooGraph) = {dog, cat, animal, vertebrate, oyster, shellfish, invertebrate, crab, poodle, monkey, banana, dalmatian, dachshund}
    E(ZooGraph) = {(vertebrate, animal), (invertebrate, animal), (dog, vertebrate), (cat, vertebrate), (monkey, vertebrate), (shellfish, invertebrate), (crab, shellfish), (oyster, shellfish), (poodle, dog), (dalmatian, dog), (dachshund, dog)}
  7. Draw a picture of ZooGraph.

  8. To tell if one element in ZooGraph has relation X to another element, you look for a path between them. Show whether the following statements are true, using your picture or the specification.

    1. dalmatian X dog

    2. dalmatian X vertebrate

    3. dalmatian X poodle

    4. banana X invertebrate

    5. oyster X invertebrate

    6. monkey X invertebrate

  9. Which of the following phrases best describes relation X in Exercise 8?

    1. “has a”

    2. “is an example of”

    3. “is a generalization of”

    4. “eats”

    Use the following graph for Exercises 10 and 11:

  10. Describe the graph pictured above, using the formal graph notation.

    V(StateGraph) =

    E(StateGraph) =

  11. In the states graph:

    1. Is there a path from Oregon to any other state in the graph?

    2. Is there a path from Hawaii to every other state in the graph?

    3. From which state(s) in the graph is there a path to Hawaii?

10.2 The Graph Interface

  1. Classify the methods defined in the WeightedGraphInterface as observers or transformers or both.

  2. It is possible to define more operations for a Graph ADT. Describe two operations that you think would be useful additions to the WeightedGraphInterface, and explain why.

  3. Suppose g has been declared to be of type WeightedGraphInterface and instantiated as an object of a class that implements that interface. Assume that g consists of seven vertices A, B, C, D, E, F, G and seven directed edges A-B, B-C, C-D, D-C, D-F, F-B, and G-E with all weights equal to 1. Show the expected output of the section of code below. Assume that capital letters A through G are of type T (the vertex type), that getToVertices returns a queue with the vertices in alphabetical order, that getUnmarked returns unmarked vertices in alphabetical order, that the null edge value is 0, and that printing a vertex displays its corresponding letter.

    System.out.println(g.isEmpty());
    System.out.println(g.weightIs(A,B));
    System.out.println(g.weightIs(B,A));
    System.out.println(g.weightIs(A,C));
    g.clearmarks(); g.markVertex(A); g.markVertex(C);
    System.out.println(g.ismarked(B));
    
    g.markVertex(g.getUnmarked()))
    System.out.println(g.isMarked(B));
    System.out.println(g.getUnmarked());
    System.out.println(g.getToVertices(D).dequeue());

10.3 Implementations of Graphs

  1. Draw the adjacency matrix for EmployeeGraph (see Exercise 5). Store the vertices in alphabetical order.

  2. Draw the adjacency matrix for ZooGraph (see Exercise 7). Store the vertices in alphabetical order.

    Note: Exercises 1822, 38, and 42 are dependent on the completion of Exercise 17.

  3. Complete the implementation of the Weighted Graph (WeightedGraph.java) that we began in this chapter by providing bodies for the methods isEmpty, isFull, hasVertex, clearMarks, markVertex, isMarked, and getUnmarked in the WeightedGraph.java file. Test the completed implementation using the UseGraph class. When implementing hasVertex, do not forget to use the equals method to compare vertices.

  4. For each of the methods in the WeightedGraph implementation, identify its order of growth efficiency.

  5. Class WeightedGraph in this chapter is to be extended to include a boolean edgeExists operation that determines whether two vertices are connected by an edge.

    1. Write the declaration of this method. Include adequate comments.

    2. Implement the method.

  6. Class WeightedGraph in this chapter is to be extended to include an int connects operation, passed two arguments a and b of type T representing two vertices in the graph (guaranteed as a precondition that both a and b are vertices) that returns a count of the number of vertices v such that v is connected to both a and b.

    1. Write the declaration of this method. Include adequate comments.

    2. Implement the method.

  7. Class WeightedGraph in this chapter is to be extended to include a removeEdge operation that removes a given edge.

    1. Write the declaration of this method. Include adequate comments.

    2. Implement the method.

  8. Class WeightedGraph in this chapter is to be extended to include a removeVertex operation that removes a vertex from the graph.

    1. Deleting a vertex is more complicated than deleting an edge from the graph. Discuss the reasons for this operation’s greater complexity. Write the declaration of this method. Include adequate comments.

    2. Implement the method.

  9. Graphs can be implemented using arrays or references. For the states graph (see Exercise 10):

    1. Show the adjacency matrix that would describe the edges in this graph. Store the vertices in alphabetical order.

    2. Show the array of adjacency lists that would describe the edges in this graph.

  10. What percent of the adjacency matrix representation of a graph consists of null edges if the graph contains

    1. 10 vertices and 10 edges?

    2. 100 vertices and 100 edges?

    3. 1,000 vertices and 1,000 edges?

  11. Assuming an adjacency matrix-based directed graph implementation as described in Section 10.3, “Implementations of Graphs,” describe an algorithm that solves each of the following problems; include an efficiency analysis of your solutions.

    1. Return the number of vertices in the graph.

    2. Return the number of edges in the graph.

    3. Return the highest number of “out edges” exhibited by a vertex; for example, for the graph in Figure 10.3 this would return 3 because Dallas has three cities adjacent to it

    4. Return the highest number of “in edges” exhibited by a vertex. For example, for the graph in Figure 10.3 this would return 3 because Atlanta has three cities it is adjacent from.

    5. Return the number of “singleton” vertices, that is, vertices that are not connected to any other vertex.

  12. Repeat Exercise 25 using the adjacency list–directed graph implementation approach represented in Figure 10.6.

  13. Specify, design, and code an undirected, weighted graph class using an adjacency matrix representation.

  14. Specify, design, and code an undirected, unweighted graph class using an adjacency matrix representation.

  15. Design and code a reference-based weighted graph class with the vertices stored in an array as in Figure 10.6. Your class should implement our WeightedGraphInterface.

  16. Design and code a reference-based weighted graph class with the vertices stored in a linked list as in Figure 10.7. Your class should implement our WeightedGraphInterface.

10.4 Application: Graph Traversals

For exercises 3136 assume getToVertices and getUnmarked return information in alphabetical order.

  1. Using the EmployeeGraph (see Exercise 5) describe both the order in which vertices would be marked and the order they would be visited/processed using the depth-first “is there a path” algorithm, when searching for a path:

    1. from Susan to Lance

    2. from Brent to John

    3. from Sander to Darlene

  2. Repeat Exercise 31 using the breadth-first algorithm.

  3. Using the StateGraph (see Exercise 10) describe both the order in which vertices would be marked and the order they would be visited/processed using the depth-first “is there a path” algorithm, when searching for a path:

    1. from Texas to Alaska

    2. from Hawaii to California

    3. from Hawaii to Texas

  4. Repeat Exercise 33 using the breadth-first algorithm.

  5. We reconfigure the Air Busters Airlines graph in Figure 10.3 so that the edges from Dallas to Austin and from Denver to Atlanta are dropped and an edge from Denver to Washington with weight 700 is added. Describe both the order in which vertices would be marked and the order they would be visited/processed using the depth-first “is there a path” algorithm, when searching for a path:

    1. from Dallas to Houston

    2. from Denver to Austin

  6. Repeat Exercise 35 using the breadth-first algorithm.

  7. Consider the following full graph of seven vertices.

    1. Recreate the drawing of the graph so that it shows only the lead-in edges to vertices that would be visited by a depth-first search starting from the vertex at the top of the image. Note that there are many possible answers.

    2. Recreate the drawing of the graph so that it shows only the lead-in edges to vertices that would be visited by a breadth-first search starting from the vertex at the top of the image. Note that there is only one possible answer.

  8. A text file contains information about a directed unweighted graph, where the vertices are strings, by listing the edges line by line. Each line contains the from-vertex, followed by the symbol #, followed by the to-vertex. For example,

    heap#priority queue

    represents an edge from the vertex “heap” to the vertex “priority queue” perhaps in this case representing the fact that a heap is used to implement a priority queue.

    Create an application that is given the name of such a text file as a command line argument, generates the corresponding graph using WeightedGraph from the ch10.graphs package, and then repeatedly prompts the user to enter a from-vertex and a to-vertex, reporting back to the user (a) one or both of the vertices does not exist or (b) whether there exists a path from the given from-vertex to the given to-vertex.

    Create a suitable input file and test your program. Submit a report.

10.5 Application: The Single-Source Shortest-Paths Problem

  1. Trace the Shortest Paths algorithm (or code) including the latter section about unreachable vertices and show what it would output for our Air Busters Airlines example (Figure 10.3) if the starting vertex is

    1. Dallas

    2. Atlanta

  2. We reconfigure the Air Busters Airlines graph in Figure 10.3 so that the edges from Dallas to Austin, Denver to Atlanta, Washington to Dallas, and Austin to Houston are dropped and an edge from Denver to Washington with weight 700 is added. Trace the Shortest Paths algorithm (or code) including the latter section about unreachable vertices and show what it would output for this new graph if the starting vertex is

    1. Austin

    2. Atlanta

  3. For the StateGraph (see Exercise 10) list the unreachable vertices for each of the vertices in the graph.

  4. Our shortestPaths method is concerned with the minimum distance between two vertices of a graph. Create a minEdges method that returns the minimum number of edges that exist on a path between two given vertices. You can put your new method in our useGraph class and use it to test your code.

  5. Counting connected components. Informally, a connected component of a graph is a subset of the vertices of the graph such that all of the vertices in the subset are connected to each other by a path. For example, the following graph consists of three connected components.

    1. Create an undirected, unweighted graph ADT (similar to Exercise 26 but you can use a linked representation if you wish).

    2. Test it.

    3. Create an application that generates a graph and counts the number of connected components. The graph can be “hard coded,” read from a file, or randomly generated.

    4. Create an application that generates graphs with 26 vertices and:

      • 4, 8, 12, 16, 20, 24, 28, 32, 36, and 40 unique random edges.

      • five graphs for each of the number of edges (so you are generating 50 random graphs altogether).

      • outputs the average number of connected components for each edge count.

    5. Submit a report that contains your code, the output, and a discussion.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.188.121