Sie sind auf Seite 1von 12

Advanced Data Structures Labwork 1

October 3, 2013
Focus: C++ implementation of an abstract datatype (ADT). Applications: Implementation of an ADT for a simple graph. Extensions: Implementation of an ADT for multigraph and pseudograph. Prerequisites: Familiarization with the standard template library of C++

Overview

The purpose of this laboratory is for you to explore how to use C++ classes to implement an abstract data type (ADT). We will develop incrementally an ADT for a simple graph. Abstract datatypes in C++ C++ provides a set of predened data types (int, char, float, and so on). Each of these predened types has a set of operations associated with it. You use these operations to manipulate values of a given type. For example, type int supports the basic arithmetic and relational operators, as well as a number of numerical functions (abs(), div(), etc.). These predened data types provide a foundation on which you construct more sophisticated data types, data types that are collections of related data items rather than individual data items. The data types created by you starting from from C++s predened data types are called abstract data types or ADTs. What is a simple graph? A simple graph is a pair (V, E ) made of a nonempty set of nodes V = {v1 , . . . , vn }. Nodes can have attributes, such as name (if the node represents a city), color, rtc. a set E of edges between nodes. Every edge has two endpoints. Edges are unoriented, which means, that an edge with endpoints vi and vj is the same as an edge with endpoints vj and vi .

Mathematicians prefer to represent an edge with endpoints vi and vj as the set {vi , vj }. This is a good mathematical representation of unoriented edges, because {vi , vj } = {vj , vi }. Computer scientists must nd (or design) a good data structure for edges. Representations of simple graphs The following simple graph represents some connections between Americal cities. G = (V, E ) with V = {SF, Denver, Chicago, Detroit, NY, Washington, LA} where SF stands for San Francisco, NY for New York, and LA for Los Angeles. E = {{SF,Denver}, {SF,LA}, {LA, Denver}, {Denver,Chicago}, {Chicago, Detroit}, {Chicago, NY}, {Chicago, Washington}, {Detroit, NY}, {Washington, N Y }}.

1.1

An abstract datatype for simple graphs

When specifying an ADT, you begin by describing what type of data items are in the ADT. Then you describe how the ADT data items are organized to form the ADTs structure. By denition, a simple graph is a pair G = (V, E ). In C++, you can specify this ADT as a class SimpleGraph in a header le SimpleGraph.h. class SimpleGraph { public: NodeSet getNodes() =0; EdgeSet getEdges() =0; }

// return a representation of the set of nodes // return a representation os the set of edges

We still have to specify what are NodeSet and EdgeSet. NodeSet should be a set of nodes. For this purpose, we can use the set container from the STL library of C++, and declare typedef set<Node> NodeSet; typedef set<Edge> EdgeSet; EdgeSet should be a set of edges. For this purpose, we can use the set container from the STL library of C++. 2

So far, the header le has the following content: #include <set> // make the standard set available

// make every name from std global using namespace std; typedef set<Node> NodeSet; typedef set<Edge> EdgeSet; class SimpleGraph public: NodeSet getNodes() =0; EdgeSet getEdges() =0; virtual ~SimpleGraph()

// return a representation of the set of nodes // return a representation os the set of edges

Next, lets start sketching the ADTs for Node and Edge. class Node { string id; public: // constructor Node(string s) : id(s) {} string getLabel() const { return id; } // comparison operators friend bool operator==(Node m, Node n) { return m.id == n.id; } friend bool operator< (Node m, Node n) { return m.id < n.id; } // string representation of a node string toString() const { return id; } }; class Edge { Node u, v; // the endpoints of the edge public: // constructor Edge(Node x, Node y) : u(x), v(y) {} }; Having specied the data items and the structure of the ADT, you then dene how the ADT can be used by specifying the operations that are associated with the ADT. For each operation, you specify what conditions must be true before the operation can be applied (its preconditions or requirements) as well as what conditions will be true after the operation has completed (its postconditions or results). The following ADT specications include operations that are useful to create a simple graph, add and remove nodes and edges, and provide general information about the graph.

2
2.1

ADTs for a Simple Graph


Node ADT
A string representing the name of the node. (More data items may be added later (e.g., color, type, etc.), when need arises.)

Data items

Operations Node(string name) Results: Constructor. Creates a node with the specied name. bool operator==(Node n) // equality operator for nodes Results: Returns true if n and this node have the same name. string getName() Results: Returns the name of the node.

2.2

Edge ADT

ADT for an edge of a simple graph. Data items Two dierent nodes representing the endpoints of an edge. (More data items may be added later (e.g., weight, color, multiplicity), when need arises.) Operations Edge(Node u, Node v) Requirements: u and v must be dierent nodes. Results: Constructor. Creates an edge with endpoints u and v . string toString() Results: Returns a string of the form (u, v ) where u and v are the names of the endpoint nodes of the edge. bool operator==(Edge e) Results: Returns true if this edge and e have the same endpoints. set<Node> getNodes() Results: Returns the set of the endpoint nodes of this edge. 4

Edges ADT
ADT for the set of edges of a simple graph. Data items The Edge representations of the edges of a simple graph. Operations Edges() Results: Constructor. Creates a representation of an empty set of edges. void add(Edge e) Results: Add edge e to this set of edges. void remove(Edge e) Results: Remove edge e from this set of edges. bool isConnected(Node u, Node v) Results: Returns true if there is an edge between nodes u and v, and false otherwise. set<Edge> toSetOfEdges() Returns a set representation of the edges stored in this ADT. set<Edge> toSetOfNodes() Returns a set representation of the nodes which are endpoints of edges.

SimpleGraph ADT
ADT for a simple graph G = (V, E ). Data items A set<Node> data item for the set of nodes V , and a set<Edge> data item for the set of edges E . Operations SimpleGraph() Results: Constructor. Creates a simple graph with no nodes and no edges. void add(Node n) Results: void add(Edge e) Requirements: The endpoint nodes of e re added to the set of nodes of this graph. Results: Adds the edge e to this simple graph. 5

void remove(Edge e) Results: Removes the edge e from this simple graph. void remove(Node n) Results: Removes the node n from this simple graph, and also all edges which have n as an endpoint. set<Node> getNodes() Results: Returns the set of nodes of this simple graph. set<Edge> getEdges() Results: Returns the set of edges of this simple graph.

Lab works
Deadline: October 10 (next lab.) 1. Implement the ADTs specied for a simple graph in C++. 2. Write a C++ program that constructs a simple graph by performing the following sequence of steps: (a) Asks the user to enter a sequence of whitespace-separated names for the nodes of the simple graph (b) Displays the menu: 1. 2. 3. 4. 5. 6. Add node Add edge Remove node Remove edge Show graph content. Exit.

Enter your choice (1-6): and waits for the user to choose an operation. (c) For choices 1 and 3: read the name of the node (a string) and perform the corresponding operation on the simple graph. For choices 2 and 4: read the names of the endpoints of the edge (two whitespaceseparated strings) and perform the corresponding operation on the simple graph. For choice 5: display the string produced by the method toString() of the simple graph. For choice 6: stop the program.

3. (Optional) A pseudo-graph is like a simple graph, except that it is allowed to have more than one edge between two nodes, and also edges from a node to itself. Edges between same nodes are distinguished by assigning dierent names to them. Specify and implement a suitable ADT for a pseudo-graph, by revising the specication of the SimpleGraph ADT accordingly. Suggestion: For pseudo-graphs, replace the Edge ADT with an ADT for edges characterized by name (a string), source node and destination node.

A
A.1

Appendix: Stu related to C++


Strings

The standard library provides a string type to complement string literals. The string type provides a variety of useful string operations, such as concatenation. For example string s1 = "Hello"; string s2 = "world"; void m1() { string s3 = s1 + ", " + s2 + "!\n"; cout << s3; } Here, s3 is initialized to the character sequence Hello, world! followed by a newline. Addition of strings means concatenation. You can add strings, string literals, and characters to a string. A very common form of concatenation is adding something to the end of a string. This kind of concatenation is directly supported by the += operation. For example void m2(string& s1, string& s2) { s1 = s1 + \n; // append newline s2 += \n; // append newline } The two ways of adding to the end of a string are semantically equivalent, but the latter is more concise and likely to be more eciently implemented. strings can be compared agains each other and against string literals. For example: string incantation; // ... void respond(const string& answer) { if (answer == incantation) { // perform magic } else if (answer == "yes") { // ...

} // ... } Another useful feature of the standard library class is the ability to manipulate substrings. For example: string name = "Niels Stroustrup"; void m3() { string s = name.substr(6, 10); name.replace(0,5,"Nicholas"); }

// s = "Stroustrup" // name becomes "Nicholas Stroustrup"

The substr() operation returns a string that is a copy of the substring indicated by its arguments. The rst argument is an index into the string (a position), and the second argument is the length of the desired substring. Since indexing starts at 0, s gets the value Stroustrup. The replace() operation replaces a substring with a value. In this case the substring starting at 0 with length 5 is Niels; it is replaced by Nicholas. Thus, the nal value of name is Nicholas Stroustrup. Note that the replacement string need not be the same size as the substring it is replacing.

A.2

Containers

A class whose main purpose is to hold objects is commonly called a container. Providing suitable containers for a given task and supporting them with useful fundamental operations are important steps in the construction of any program. We will illustrate some of the standard librarys most useful containers on a simple program for keeping names and telephone numbers. A.2.1 vector

Perhaps for many programmers, a built-in array of (name, number) pairs would seem to be a suitable starting point: struct Entry { string name; int number; } Entry phone_book[1000]; void print_entry(int i) { // simple use cout << phone_book[i].name << << phone_book[i].number << \n; } The drawback of this approach is that arrays have a xed size. Choosing a large size wastes space, whereas choosing a smaller size may produce overows. We can avoid this deciency by using the vector container provided by the standard library. 8

vector<Entry> phone_book(1000); void print_entry(int i) { // simple use, exactly as for array cout << phone_book[i].name << << phone_book[i].number << \n; } void add_entries(int n) { // increase size by n phone_book.resize(phone_book.size() + n); } The vector member function size() gives the number of elements. Note the use of parentheses in the denition of phone book. We made a single object of type vector<Entry> and supplied its initial size as an initializer. This is very dierent from declaring a built-in array: vector<Entry> book(1000); // vector of 1000 elements vector<Entry> books[1000]; // 1000 empty vectors A.2.2 vector

A vector is a single object that can be assigned. For example: void f(vector<Entry>& v) { vector<Entry> v2 = phone_book; v = v2; // ... } Assigning a vector involves copying its elements. Thus, after the initialization and assignment in f(), v and v2 each hold a separate copy of every Entry in the phone book. When a vector holds many elements, such innocent-looking assignments and initializations can be very expensive. Where copying is undesirable, references or pointers should be used.

A.3

list

Insertion and deletion of phone book entries could be common. Therefore, a list could be more appropriate than a vector for representing a simple phone book. For example list<Entry> phone_book; When ve use a list, we tend not to access elements using subscripting the way we commonly do for vectors. Instead, we might search the list for an element with a given value. This kind of search can be performed with a list iterator, as illustrated below: void print_entry(const string s) { typedef list<Entry>::const_iterator LI; for (LI i = phone_book.begin(); i!=phone_book.end(); ++i) { const Entry& e = *i; // reference used as a shorthand if (s == e.name) { 9

cout << e.name << << e.number << \n; return; } } } The search for s starts at the beginning of the list and proceeds until either s is found or the end is reached. Every standard library container provides the functions begin() and end(), which return an iterator to the rst and one-past-the-last element, respectively. Given an iterator i, ++i advances i to refer to the next element. Given an iterator i, the element it refers to is *i. A user need not know the exact type of the iterator for a standard container. That iterator type is part of the denition of a container and can be referred to by name. When we dont need to modify an element of the container, const iterator is the type we want. Otherwise, we use the plain iterator type. The following code snippet illustrates how easy it is to add elements to a list and remove element from a list: void f(const Entry& e, list<Entry>::iterator i, list<Entry>::iterator p) { phone_book.push_front(e); // add at beginning phone_book.push_back(e); // add at end phone_book.insert(i,e); // add before the element referred to by i phone_book.erase(p); // remove the element referred to by p } A.3.1 map

Writing code to look up a name in a list of (name, number) pairs is tedious. Also, a linear search is quite inecient for long lists. Fortunately, there are other data structures that directly support ecient insertion, deletion, and searching based on values. The map type provided by the standard library is such a data structure. More precisely, map is a container of pairs of values. For example: map<string,int> phone_book; A map is also known as an associative array or a dictionary. When indexed by a value of its rst type (called the key), a map returns the corresponding value of the second type (called the value or the mapped type). For example: void print_entry(const string& s) { if (int i = phone_book[s]) cout << s << << i << \n; } If no match was found for the key s, default value is returned from the phone book. The default value of an integer type in a map is 0. Here, the assumption is that 0 is not a valid telephone number.

10

Other standard containers map, list, and vector have dierent strengths and weaknesses to represent a phone book. For example, subscripting a vector is cheap and easy, but inserting an element between two elements tends to be expensive. A list has exactly the opposite properties. A map resembles a list of (key, value) pairs except that it is optimized for nding values based on keys. Often, C++ programmers can select a general container from the standard library that best serves the needs of their application: vector<T> list<T> queue<T> stack<T> deque<T> priority queue<T> set<T> multiset<T> map<key,val> multi map<key,val> Standard Container Summary A variable-sized vector A doubly-linked list A queue A stack A double-ended queue A queue sorted by value A set A set in which a value can occur many times An associative array A map in which a key can occur many times

These containers are dened in namespace std and presented in headers <vector>, <list>, <map>, etc. The standard containers and their basic operations are designed to be similar from a notational point of view. Also, the meanings of the operations are equivalent for the various containers. In general, basic operations apply to every kind of container. For example, push back() can be used (reasonably eciently) to add elements to the end of a vector as well as for a list, and every container has a size() member function that tells the number of elements. This notational and semantic uniformity enables programmers to provide new container types to be used like the standard containers. Also, it allows us to specify algorithms independently of individual container types.

A.4

Algorithms

Standard structures, such as a list or a vector, are not very useful on their own. To use them, we need operations for basic access such as adding and removing elements. Also, we often wish to sort their content, extract subsets, remove elements, search for objects, etc. Therefore, the standard library provides the most common algorithms for containers in addition to providing the most common container types. For example, we can dene == and < for Entry, sort a vector of Entrys, and place a copy of each unique vector element on a list: bool operator==(const Entry& a, const Entry& b) { return a.name == b.name; } bool operator<(const Entry& a, const Entry& b) { return a.name < b.name; } void f(vector<Entry>& ve, list<Entry>& le) { sort(ve.begin(), ve.end()); unique_copy(ve.begin(), ve.end(), le.begin()); }

11

The standard algorithms are expressed in terms od sequences of elements. A sequence of elements is represented by a pair of iterators specifying the rst element and the one-beyondthe-last element in the sequence. In the previous example, sort() sorts the sequence from ve.begin() to ve.end() which just happens to be all the elements of a vector. For writing, one only needs to specify the rst element to be written. If more than one element is written, the elements following that initial element will also be overwritten. If we wanted to add the new elements to the end of a container, we could have written: void f(vector<Entry>& ve, list<Entry>& le) { sort(ve.begin(),ve.end()); unique_copy(ve.begin(),ve.end(),back_inserter(le)); }

// append to le

A back inserter() adds elements at the end of a container, extending the container to make room for them. Forgetting to use a back inserter() when appending can lead to errors. For example: void f(vector<Entry>& ve, list<Entry>& le) { copy(ve.begin(),ve.end(),le); // error: le is not an iterator copy(ve.begin(),ve.end(),le.end()); // bad: writes beyond the end copy(ve.begin(),ve.end(),le.begin()); // overwrite elements } A.4.1 Sequences and iterators

In order to parameterize algorithms by containers, the standard library relies on the notion of sequence and on the possibility to manipulate sequences with iterators. Here is a graphical representation of the notion of a sequence:

A sequence has a beginning and an end. An iterator refers to an element, and provides an operation that makes the it erator refer to the next element of the sequence. The end of a sequence is an iterator that refers one beyond the last element in the sequence. The physical representation of the end may be a sentinel element, but it doesnt have to be. In fact, the point is that this notion of sequences covers a wide variety of representations, including lists and arrays. Iterators introduce a standard notations for the operations of element access and sequence traversal. Given an iterator, the deference operator * means access an element through the iterator, and the increment operator ++ means make the iterator refer to the next element.

References
B. Stroustrup. The C++ programming language. Special Edition. April 2009.

12

Das könnte Ihnen auch gefallen