In this section, we present three example programs that illustrate the use of maps and multimaps. These examples deal with a telephone database, graphs, and a concordance.
NOTE -- The complete example program is in the file tutorial tele.cpp.
A maintenance program for a simple telephone database is a good application for a map. The database is simply an indexed structure, where the name of the person or business (a std::string) is the key value, and the telephone number (a long) is the associated entry. We might write such a class as follows:
typedef std::map<std::string, long, std::less<std::string>, std::allocator<std::pair<const std::string, long> > > friendMap; typedef friendMap::value_type entry_type; class telephoneDirectory { public: void addEntry (std::string name, long number) { database[name] = number; } void remove (std::string name) { database.erase (name); } void update (std::string name, long number) { remove (name); addEntry (name, number); } void displayDatabase () { std::for_each (database.begin (), database.end (), printEntry); } void displayPrefix (int); void displayByPrefix (); private: friendMap database; };
Simple operations on our database are directly implemented using member functions of the map. Adding an element to the database is simply an insert(), removing an element is an erase(), and updating is a combination of the two. To print all the entries in the database we can use the for_each() algorithm, and apply the following simple utility routine to each entry:
void printEntry (const entry_type & entry) { std::cout << entry.first << ":" << entry.second << std::endl; }
We now use a pair of slightly more complex operations to illustrate how a few of the algorithms described in Chapter 13 can be used with maps. Suppose we want to display all the phone numbers with a certain three-digit initial prefix. We use the std::find_if() function, which is not to be confused with the find() member function of map, to locate the first entry. Starting from this location, subsequent calls on std::find_if() uncover each successive entry:
void telephoneDirectory::displayPrefix (int pfx) { std::cout << "Listing for prefix " << pfx << std::endl; friendMap::iterator where = std::find_if (database.begin (), database.end (), checkPrefix (pfx)); while (where != database.end ()) { printEntry (*where); where = std::find_if (++where, database.end (), checkPrefix (pfx)); } std::cout << "end of prefix listing" << std::endl; }
For the predicate to this operation, we require a boolean function that takes only a single argument, the pair representing a database entry, and tells us whether or not it is in the given prefix. There is no obvious candidate function, and in any case the test prefix is not being passed as an argument to the comparison function. The solution to this problem is to employ a common C++ Standard Library technique: define the predicate function as an instance of a class, and store the test predicate as an instance variable in the class, initialized when the class is constructed. The desired function is defined as the function call operator for the class:
long prefix (const entry_type& entry) { return entry.second / 10000; } class checkPrefix { public: checkPrefix (long p) : testPrefix (p) { } long testPrefix; bool operator () (const entry_type& entry) { return prefix(entry)==testPrefix; } };
Our final example displays the directory sorted by prefix. It is not possible to alter the order of the maps themselves. Instead, we create a new map with the element types reversed and copy the values into the new map, which orders the values by prefix. Once the new map is created, it is printed:
typedef std::map<long, std::string, std::less<long>, std::allocator<std::pair<const long, std::string> > > sortedMap; typedef sortedMap::value_type sorted_entry_type; void telephoneDirectory::displayByPrefix () { std::cout << "Display by prefix" << std::endl; sortedMap sortedData; for (friendMap::iterator i = database.begin (); i != database.end (); i++) sortedData.insert (sortedMap::value_type ((*i).second, (*i).first)); std::for_each (sortedData.begin (), sortedData.end (), printSortedEntry); std::cout << "end display by prefix" << std::endl; }
Here is the function used to print the sorted entries:
void printSortedEntry (const sorted_entry_type & entry) { std::cout << entry.first << ":" << entry.second << std::endl; }
NOTE -- The complete version of this program is in the file graph.cpp.
A map whose elements are themselves maps is a natural representation for a directed graph. For example, suppose we use strings to encode the names of cities, and we wish to construct a map where the value associated with an edge is the distance between two connected cities. We could create such a graph as follows:
typedef std::map<std::string, int, std::less<std::string>, std::allocator<std::pair<const std::string, int> > > stringVector; typedef std::map<std::string, stringVector, std::less<std::string>, std::allocator<std::pair<const std::string, stringVector> > > graph; // define strings for city names std::string pendleton ("Pendleton"); std::string pensacola ("Pensacola"); std::string peoria ("Peoria"); std::string phoenix ("Phoenix"); std::string pierre ("Pierre"); std::string pittsburgh ("Pittsburgh"); std::string princeton ("Princeton"); std::string pueblo ("Pueblo"); // declare the graph that holds the map graph cityMap; // add edges to the graph cityMap[pendleton][phoenix] = 4; cityMap[pendleton][pueblo] = 8; cityMap[pensacola][phoenix] = 5; cityMap[peoria][pittsburgh] = 5; cityMap[peoria][pueblo] = 3; cityMap[phoenix][peoria] = 4; cityMap[phoenix][pittsburgh] = 10; cityMap[phoenix][pueblo] = 3; cityMap[pierre][pendleton] = 2; cityMap[pittsburgh][pensacola] = 4; cityMap[princeton][pittsburgh] = 2; cityMap[pueblo][pierre] = 3;
The type stringVector is a map of integers indexed by text strings. The type graph is, in effect, a two-dimensional sparse array, indexed by strings and holding int values. A sequence of assignment statements initializes the graph.
A number of classic algorithms can be used to manipulate graphs represented in this form. One example is Dijkstra's shortest-path algorithm. Dijkstra's algorithm begins from a specific city given as an initial location. A priority_queue of distance/city pairs is then constructed, and initialized with the distance from the starting city to itself (namely, zero). The definition for the distance pair datatype is as follows:
struct DistancePair { unsigned first; std::string second; DistancePair () : first (0) {} DistancePair (unsigned f, const std::string& s) : first (f), second (s) {} }; bool operator< (const DistancePair& lhs, const DistancePair& rhs) { return lhs.first < rhs.first; }
In the algorithm that follows, note how the conditional test is reversed on the priority_queue, because at each step we wish to pull the smallest, and not the largest, value from the collection. On each iteration around the loop we pull a city from the queue. If we have not yet found a shorter path to the city, the current distance is recorded, and we can compute the distance from this city to each of its adjacent cities by examining the graph. This process continues until the priority_queue becomes exhausted:
void shortestDistance (graph& cityMap, std::string& start, stringVector& distances) { // Process a priority queue of distances to nodes. std::priority_queue<DistancePair, std::vector<DistancePair, std::allocator<DistancePair> >, std::greater<DistancePair> > que; que.push (DistancePair (0, start)); while (! que.empty()) { // Pull nearest city from queue. int distance = que.top().first; std::string city = que.top().second; que.pop(); // If we haven't seen it already, process it. if (0 == distances.count (city)) { // Then add it to shortest distance map. distances[city] = distance; // And put values into queue. const stringVector& cities = cityMap[city]; stringVector::const_iterator start = cities.begin (); stringVector::const_iterator stop = cities.end (); for (; start != stop; ++start) que.push(DistancePair (distance + (*start).second, (*start).first)); } } }
Notice that this relatively simple algorithm makes use of vector (Chapter 5), map (Chapter 9), string (Chapter 12), and priority_queue (Chapter 11).
A concordance is an alphabetical listing of words in a text that shows the line numbers on which each word occurs.
NOTE -- The executable version of this program is in the file concord.cpp.
We develop a concordance to illustrate the use of the map and multimap container classes. The data values are maintained in the concordance by a multimap, indexed by word (a string) and holding the line numbers (int). A multimap is used because the same word is expected to appear on different lines; indeed, discovering such connections is one of the primary purposes of a concordance. Another possibility would be to use a map and use a set of integer elements as the associated values.
class concordance { typedef std::multimap<std::string,int, std::less<std::string>, std::allocator<std::pair<const std::string, int> > > wordDictType; public: void addWord (std::string, int); void readText (std::istream &); void printConcordance (std::ostream &); private: wordDictType wordMap; };
The creation of the concordance is divided into two steps: the program generates the concordance by reading lines from an input stream, then prints the result on the output stream. This is reflected in the two member functions readText() and printConcordance(). The first of these, readText(), is written as follows:
void concordance::readText (std::istream & in) { std::string line; for (int i = 1; std::getline (in, line); i++) { allLower (line); std::list<std::string, std::allocator<std::string> > words; split (line, " , .;:", words); std::list<std::string, std::allocator<std::string> >::iterator wptr; for (wptr = words.begin (); wptr != words.end (); ++wptr) addWord (*wptr, i); } }
Lines are read from the input stream one by one. The text of the line is first converted into lower case, then the line is split into words using the function split() described in Section 12.3. Each word is then entered into the concordance. The method used to enter a value into the concordance is as follows:
void concordance::addWord (std::string word, int line) { // First get range of entries with same key. wordDictType::iterator low = wordMap.lower_bound (word); wordDictType::iterator high = wordMap.upper_bound (word); // Loop over entires, see if any match current line. for ( ; low != high; ++low) if ( (*low).second == line) return; // Didn't occur, add now. wordMap.insert (wordDictType::value_type (word, line)); }
The major portion of addWord() is concerned with ensuring that values are not duplicated in the word map if the same word occurs twice on the same line. To assure this, the range of values matching the key is examined, each value is tested, and if any match the line number then no insertion is performed. It is only if the loop terminates without discovering the line number that the new word/line number pair is inserted.
The final step is to print the concordance. This is performed in the following fashion:
void concordance::printConcordance (std::ostream & out) { std::string lastword (""); wordDictType::iterator pairPtr; wordDictType::iterator stop = wordMap.end (); for (pairPtr = wordMap.begin (); pairPtr != stop; ++pairPtr) // If word is same as previous, just print line number. if (lastword == (*pairPtr).first) out << " " << (*pairPtr).second; else { // First entry of word. lastword = (*pairPtr).first; std::cout << std::endl << lastword << ": " << (*pairPtr).second; } std::cout << std::endl; }
An iterator loop is used to cycle over the elements being maintained by the word list. Each new word generates a new line of output; thereafter, line numbers appear separated by spaces. For example, if the input was the text:
It was the best of times, it was the worst of times.
The output, from best to worst, would be:
best: | 1 |
it: | 1 2 |
of: | 1 2 |
the: | 1 2 |
times: | 1 2 |
was: | 1 2 |
worst: | 2 |