Project 1: Adjacency Lists

Due: Tuesday, February 18, before 9:00 pm


Addenda


Objectives


Introduction

In computer science and discrete mathematics, a graph is not a plot of y = f(x). A graph has vertices and edges. Here's a simple example:

The vertices are the circles, and the edges are the lines between the vertices. It is common to use the words “node” and “vertex” interchangeably.

In the example above, the set of vertices is \(\{0,1,2,3,4\}\) and the set of edges is \(\{(0,1), (0,3), (1,2), (1,4), (2, 4), (3,4), (4,3)\}\). It is common to use the ordered pair notation \((u, v)\) to represent an edge. This graph is an example of a directed graph: each edge has a direction. The edge \((u, v)\) goes from vertex \(u\) to vertex \(v\).

Note that it is possible to have edges in both directions, such as \((3,4)\) and \((4,3)\) in the example. Also, a vertex is allowed to have an edge to itself, called a self-edge. There are certain applications in which it is useful to have self-edges.

A weighted graph has numeric weights associated with the edges. This may seem like a strange thing to do, but it is natural in many applications. For example, suppose your graph represents nodes in a network and edges represent network connections; then the edge weights could indicate the bandwidth of the connection. In the figure below, we've added some arbitrary edge weights to our original example.

A weighted edge from \(u\) to \(v\) with weight \(w\) is represented by a tuple \((u,v,w)\). The weighted edges of the example graph are \(\{(0,1,5.2), (0,3,1.0), (1,2,2.1), (1,4,2.8), (2, 4, 3.7), (3,4,4.4), (4,3,0.3)\}\).

One common way to store a graph is using an adjacency list data structure. Typically, this is an array of linked lists, one list for each vertex; however, for this project, we will use dynamically-resized arrays rather than linked lists. The following figure represents the adjacency list data structure for the sample weighted graph.

Here the first dynamic array, indexed by 0, has the neighbors of vertex 0. The neighbors of a vertex \(u\) are the vertices \(v\) such that \((u, v)\) is an edge in the graph. The dynamic array for vertex \(u\) will contain all neighbors \(v\) of \(u\) along with the edge weights. For example, the dynamic array for vertex 1 in our sample weighted graph contains the pairs \((2,2.1)\) and \((4,2.8)\).

Dynamic Arrays

A limitation of C/C++ arrays is their fixed size: once an array of \(n\) elements is created, it is forever limited to storing at most \(n\) elements. Thus we frequently create arrays that are larger than we expect to need, which is an inefficient use of memory. Dynamic arrays are able to resize themselves as necessary, expanding if we attempt to insert into an array that is full, and contracting when only a small portion of the array is being used. The STL vector container is an implementation of dynamic arrays.

The size of a dynamic array is the number of elements currently in the array and the capacity is the amount of allocated space. There are efficient strategies for expanding and contracting a dynamic array:

See Section 6.1 of the textbook for more information about dynamic arrays (called extendable arrays in the textbook).


Assignment

Your assignment is to implement the sparse adjacency list data structure Graph that is defined in the header file Graph.h. The dynamic arrays that store the neighbor lists are implemented in a second class, EntryList, which is defined in EntryList.h. Thus, to complete the Graph class, you must first implement the EntryList class.

Additionally, you must write a test program that fully exercises your implementations of both classes. The test program must be named mytest.cpp.

Provided Files

All of these files are also available on GL in the directory:

/afs/umbc.edu/users/c/m/cmarron/pub/www/cs341.s20/projects/proj1files/

About Iterators

Both the EntryList and Graph classes include iterators. The purpose of an iterator is to provide programmers a uniform way to iterate through all items of a data structure using a for loop. For example, using the Graph class, we can iterate thru the neighbors of vertex 4 using:

Graph::NbIterator nit ; for (nit = G.nbBegin(4); nit != G.nbEnd(4) ; nit++) { cout << *nit << " " ; } cout << endl ;

The idea is that nit starts at the beginning of the data for vertex 4 and is advanced to the next neighbor by the ++ operator. The for loop continues as long as we have not reached the end of the data for vertex 4. We check this by comparing against a special iterator for the end, nbEnd(4). This requires the NbIterator class to implement the ++, != and * (dereference) operators.

Similarly, the Graph class allows us to iterate through all edges of a graph using a for loop like:

Graph::EgIterator eit ; tuple<int,int,weight_t> edge ; for (eit = G.egBegin() ; eit != G.egEnd() ; eit++) { edge = *eit ; // get current edge cout << "(" << get<0>(edge) << ", " << get<1>(edge) << ", " << get<2>(edge) << ") " ; } cout << endl ;

Since a program may use many data structures and each data structure might provide one or more iterators, it is common to make the iterator class for a data structure an inner class. Thus, in the code fragments above, nit and eit are declared as Graph::NbIterator and Graph::EgIterator objects, not just NbIterator and EgIterator objects.

The EntryList Class

An EntryList object is a dynamically-resized array of Entry objects; an Entry stores a vertex and weight (the Entry class is fully implemented in EntryList.h). The EntryList class is required to store Entry objects in order of increasing vertex number, and vertex numbers must be unique.

In addition to providing basic creation, retrieval, update, and deletion operations, the EntryList class provides an iterator which can be used to iterate through the elements of the list. For example, the following code would print all the entries in an EntryList named EL:

EntryList::Iterator itr ; for ( itr = EL.begin(); itr != EL.end() ; itr++ ) { cout << *itr << " " ; } cout << endl ;

You must complete the implementations of the following functions of the EntryList class:

The EntryList Iterator Class

Additionally, you must implement the EntryList::Iterator class. The iterator is implemented as a inner class of EntryList.

The Graph Class

The Graph class implements the adjacency list representation of a graph. It uses EntryList objects to store neighbor lists.

The Graph class has two iterator classes: a neighbor iterator (NbIterator) and an edge iterator (EgIterator). Given a vertex v in the graph, the neighbor iterator can be used to iterate over all neighbors of v; it is really just a wrapper on the EntryList::Iterator class. The edge iterator can be used to iterate over all the edges in the graph. The edge iterator implementation is a bit more involved, but still should make use of the iterator for EntryList.

The following functions must be implemented to complete the Graph class:

The Graph Iterator Classes

Additionally, you must implement the Graph::EgIterator and Graph::NbIterator classes. The iterators are inner classes of Graph.

Graph::EgIterator

Graph::NbIterator


Additional Requirements

Be sure to read the function descriptions carefully — your implementation is expected to behave as described in this document. Following are additional project requirements.

Requirement: You may not use any Standard Template Library (STL) classes other than pair and tuple in the implementation of EntryList and Graph. You may use STL classes in mytest.cpp.

Requirement: your code must compile with the original Graph.h header file. You are not allowed to make any changes to this file.

Requirement: You may add private helper functions to the EntryList class, but they must be declared in EntryList.h. No other modifications to EntryList.h are permitted.

Requirement: per the course coding standards, your code must compile with g++ on the GL servers without using any compilation flags.

Requirement: a program fragment with a for loop that uses your NbIterator must have worst case running time that is proportional to the number of neighbors of the given vertex.

Requirement: a program fragment with a for loop that uses your EgIterator must have worst case running time that is proportional to the number of vertices in the graph plus the number of edges in the graph.


Implementation Notes


Testing

Following is a non-exhaustive list of tests to perform on your implementation.

EntryList

Basic tests of insert(), update(), remove(), and getEntry:

Tests of the copy constructor and assignment operator:

Miscellaneous tests:

Graph

Basic tests of constructor, addEdge(), and removeEdge():

Tests of the copy constructor and assignment operator:

Tests of the edge iterator:

Tests of the neighbor iterator:


What to Submit

You must submit the following files to the proj1 directory.

You do not need to submit Graph.h because it should not have changed. If you do happen to place a copy of Graph.h in your submission directory, it will be replaced by a copy of the original version.

If you followed the instructions in the Project Submission page to set up your directories, you can submit your code using this Unix command command.

cp EntryList.h EntryList.cpp Graph.cpp mytest.cpp ~/cs341proj/proj1/