Top of document
©Copyright 1999 Rogue Wave Software

Example Programs

We present three example programs that illustrate the use of maps and multimaps. These are a telephone database, graphs, and a concordance.

A Telephone Database

Obtaining the Sample Program

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 string) is the key value, and the telephone number (a long) is the associated entry. We might write such a class as follows:

typedef map<string, long, less<string> > friendMap;
 typedef friendMap::value_type entry_type;
 
 class telephoneDirectory {
 public:
    void addEntry (string name, long number)   // add new entry to
                                               // database
       { database[name] = number; }
       
    void remove (string name)   // remove entry from database
       { database.erase(name); }
    
    void update (string name, long number)   // update entry
       { remove(name); addEntry(name, number); }
       
    void displayDatabase()   // display entire database
       { for_each(database.begin(), database.end(), printEntry); }
    
    void displayPrefix(int);   // display entries that match prefix
    
    void displayByPrefix();   // display database sorted by prefix
    
 private:
    friendMap database;
 };
 

Simple operations on our database are directly implemented by map commands. 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)
    { cout << entry.first << ":" << entry.second << endl; }
 

We will 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 wanted to display all the phone numbers with a certain three digit initial prefix[1] . We will use the find_if() function (which is different from the find() member function in class map) to locate the first entry. Starting from this location, subsequent calls on find_if() will uncover each successive entry.

void telephoneDirectory::displayPrefix(int prefix)
 {
    cout << "Listing for prefix " << prefix << endl;
    friendMap::iterator where;
    where = 
       find_if (database.begin(), database.end(),
              checkPrefix(prefix));
    while (where != database.end()) {
       printEntry(*where);
       where = find_if (++where, database.end(),
              checkPrefix(prefix));
       }
    cout << "end of prefix listing" << 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 technique that is commonly used with the standard library, defining the predicate function as an instance of a class, and storing the test predicate as an instance variable in the class, initialized when the class is constructed. The desired function is then defined as the function call operator for the class:

int prefix(const entry_type & entry)
    { return entry.second / 10000; }
 
 class checkPrefix {
 public:
    checkPrefix (int p) : testPrefix(p) { }
    int testPrefix;
    bool operator () (const entry_type & entry)
       { return prefix(entry) == testPrefix; }
 };
 

Our final example will be to display the directory sorted by prefix. It is not possible to alter the order of the maps themselves. So instead, we create a new map with the element types reversed, then copy the values into the new map, which will order the values by prefix. Once the new map is created, it is then printed.

typedef map<long, string, less<long> > sortedMap;
 typedef sortedMap::value_type sorted_entry_type;
 
 void telephoneDirectory::displayByPrefix()
 {
    cout << "Display by prefix" << endl;
    sortedMap sortedData;
    friendMap::iterator itr;
    for (itr = database.begin(); itr != database.end(); itr++)
       sortedData.insert(sortedMap::value_type((*itr).second, 
             (*itr).first));
    for_each(sortedData.begin(), sortedData.end(),
             printSortedEntry);
 }
 

The function used to print the sorted entries is the following:

void printSortedEntry (const sorted_entry_type & entry) 
       { cout << entry.first << ":" << entry.second << endl; }
 

Graphs

Obtaining the Sample Program

A map whose elements are themselves maps are 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 map<string, int> stringVector;
 typedef map<string, stringVector> graph;
 
 const string pendleton("Pendleton");   // define strings for 
                                        // city names
 const string pensacola("Pensacola");
 const string peoria("Peoria");
 const string phoenix("Phoenix");
 const string pierre("Pierre");
 const string pittsburgh("Pittsburgh");
 const string princeton("Princeton");
 const string pueblo("Pueblo");
 
 graph cityMap;      // declare the graph that holds the map
 
 cityMap[pendleton][phoenix] = 4;   // add edges to the graph
 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 strings. The type graph is, in effect, a two-dimensional sparse array, indexed by strings and holding integer 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 data type is as follows:

struct DistancePair {
    unsigned int first;
    string second;
    DistancePair() : first(0) { }
    DistancePair(unsigned int f, const 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 by examining the graph we can compute the distance from this city to each of its adjacent cities. This process continues until the priority queue becomes exhausted.

void shortestDistance(graph & cityMap, 
       const string & start, stringVector & distances)
 {
    // process a priority queue of distances to cities
    priority_queue<DistancePair, vector<DistancePair>, 
                   greater<DistancePair> > que;
    que.push(DistancePair(0, start));
    
    while (! que.empty()) {
       // pull nearest city from queue
       int distance = que.top().first;
       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 vectors, maps, strings and priority_queues. priority_queues are described in greater detail in Chapter 11.

A Concordance

Obtaining the Sample Program

A concordance is an alphabetical listing of words in a text, that shows the line numbers on which each word occurs. We develop a concordance to illustrate the use of the map and multimap container classes. The data values will be maintained in the concordance by a multimap, indexed by strings (the words) and will hold integers (the line numbers). A multimap is employed because the same word will often appear on multiple different lines; indeed, discovering such connections is one of the primary purposes of a concordance. Another possibility would have been to use a map and use a set of integer elements as the associated values.

class concordance {
    typedef multimap<string, int less <string> > wordDictType;
 public:
    void addWord (string, int);
    void readText (istream &);
    void printConcordance (ostream &);
    
 private:
    wordDictType wordMap;
 };
 

The creation of the concordance is divided into two steps: first the program generates the concordance (by reading lines from an input stream), and then the program 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 (istream & in)
 {
    string line;
    for (int i = 1; getline(in, line, '\n'); i++) {
       allLower(line);
       list<string> words;
       split (line, " ,.;:", words);
       list<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 Chapter 19 (Example Program of auto_ptr). Each word is then entered into the concordance. The method used to enter a value into the concordance is as follows:

void concordance::addWord (string word, int line)
 {
    // see if word occurs in list 
    // first get range of entries with same key
    wordDictType::iterator low = wordMap.lower_bound(word);
    wordDictType::iterator high = wordMap.upper_bound(word);
    // loop over entries, 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 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 (ostream & out)
 {
    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;
          cout << endl << lastword << ": " << (*pairPtr).second;
          }
    cout << endl; // terminate last line
 }
 

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. If, for example, 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: 1


Top of document