CSE2305 - Object-Oriented Software Engineering
Assessment

 

Assignment 2: solutions


This assignment was due Friday, 1 September 2006.
Total marks: 50.
Contribution to final mark: 5%.

Synopsis


Question 1

[30 marks]

The question required an associative array with each element in the array indexed by a unique key (a string) and storing a Colour value. Your were asked to implement this class using a hash table that can handle collisions. As with assignment 1, you should separate class interface (.h file) from implementation (.cpp file). In this solution, the constructor provides a hint to the size of the table, but if no size argument is provided a default value is used (hash tables prefer table sizes that are prime numbers).
Colour.h
#ifndef _CLS_COLOUR_
#define _CLS_COLOUR_         
#include <iostream>
class Colour
{
   private:
      int red, green, blue;
   
   public: 
      Colour(void) { red=green=blue=0; }
      Colour(int newRed, int newGreen, int newBlue);
   
   // no destructor needed (no dynamic memory allocation was done)
   // use default copy constructor (assign each value to its counterpart)
   
      void Set(int newRed, int newGreen, int newBlue);
   
   // inline these three functions for speedy return with no overhead...
      int Red(void) const { return red; }
      int Green(void) const { return green; }
      int Blue(void) const { return blue; }
   
      std::ostream& Dump(std::ostream& s);
   
      friend bool operator== (const Colour& rhs, const Colour& lhs);
};
#endif
Colour.cpp
#include "Colour.h"
#include <cassert>       
Colour::Colour(int newRed, int newGreen, int newBlue)
{
   // An alternative validity test would work here using just if/else
   // but assert has several advantages, especially in a constructor.
   // One useful advantage is that you can turn it off (look up how to do this).
   // Beware of using assert for inline functions (this is not an inline function).
   
   assert ((newRed>=0) && (newRed<=255));
   assert ((newGreen>=0) && (newGreen<=255));
   assert ((newBlue>=0) && (newBlue<=255));
   
   red=newRed;
   green=newGreen;
   blue=newBlue;
}
   
void Colour::Set(int newRed, int newGreen, int newBlue)
{
   assert ((newRed>=0) && (newRed<=255));
   assert ((newGreen>=0) && (newGreen<=255));
   assert ((newBlue>=0) && (newBlue<=255));
   
   red=newRed; green=newGreen; blue=newBlue;
}
std::ostream& Colour::Dump(std::ostream& s)
{
   s << "[ " << red << ", " << green << ", " << blue << " ]";
   return s;
}
// Note the use of the friend "operator ==".
// We could have implemented equivalence without using the "==" operator, but this method is more elegant.
// Look up "friends" in your text book... we'll discuss them shortly in a lecture.
bool operator== (const Colour& rhs, const Colour& lhs)
{
   if ((lhs.red==rhs.red) && (lhs.green==rhs.green) && (lhs.blue==rhs.blue))
   { return true; }
   
   return false;
}

Array.h
#ifndef ASSOCARRAY_H
#define ASSOCARRAY_H
#include "List.h"
#include "Colour.h"
using namespace std;
typedef unsigned int uint;
typedef string KeyType; // keyType is a string (the colour name)
typedef Colour ValueType; // valueType is a Colour (an objct of class Colour defined in Colour.h)
class Array
{
   public:
      // default table size for new Arrays
      static const uint DefaultTableSize = 101;
    Array(uint sizeHint = DefaultTableSize);
   ~Array();
    void Insert(const KeyType& key, const ValueType& value);
    bool Exists(const KeyType& key);
    ValueType Find(const KeyType& key) ; // Returns a copy, not a reference to the object stored in the table.
    ostream& Dump(ostream& s);
  protected:
     // these data members and functions are for hashing the key (they were given in the question)
     static const uint shift = 6; // char size
     static const uint mask = ~0U << ((sizeof(uint) * 8) - shift);
     uint Hash(const KeyType& key) const;
  private:
     uint myTableSize;
     List *myTable;
};
#endif // ARRAY_H
Array.cpp
#include "AssocArray.h"
// constructor implementation
// allocates a hash table of given size
Array::Array(uint sizeHint) : myTableSize(sizeHint) 
{
   // allocate space for a new associative array (hash table)
   myTable = new List[sizeHint];
}
// destructor
Array::~Array() 
{
   delete [] myTable;
}
// Insert
// Inserts both key and data into the list at the hashed location in
// the array. Note that the function does not check to see if a key
// already exists, but to do so would be trivial.
void Array::Insert(const KeyType& key, const ValueType& value) { myTable[Hash(key)].Insert(KeyValuePair(key,value)); }
// Exists
// Checks to see if the supplied key exists
bool Array::Exists(const KeyType& key) 
{
   List& l = myTable[Hash(key)];
   KeyValuePair data;

   for (l.First(); l.GetCurrent(data); l.Next())
   if (key == data.first) return true;
   return false;
}
// Find
// Find a given Key
// Return a *copy* of the object found in the table with matching key. This is important
// for returning the void-constructed ValueType since we wouldn't want to return a
// reference to a local object that wil disappear when Find() exits.
//
ValueType Array::Find(const KeyType& key) 
{
   List& l = myTable[Hash(key)];
   
   KeyValuePair data;
   for (l.First(); l.GetCurrent(data); l.Next())
   {
      if (key == data.first)
     {
          return data.second;
      }
   }
   
   return ValueType(); // if you get here, the key wasn't found so return a void-constructed object
}
// Hash
// hashes a string to an unsigned integer (the index into the array)
// This code was fully given on the question sheet as a "hint"
//
uint Array::Hash(const KeyType& key) const 
{
   uint result = 0;
   for (uint i = 0; i < key.size(); ++i)
   result = (result & mask) ^ (result << shift) ^ key[i];
   // cout << "hash(" << key << ") = " << result % myTableSize << endl; 
   return result % myTableSize;
}
// Dump
// Utility function to dump hash table contents to the supplied stream
ostream & Array::Dump(ostream& s)
{
   int min = numeric_limits<int>::max(); // init the 'min' value to the highest value an int can store
   int max = 0, count = 0;
   
   for (uint i = 0; i < myTableSize; ++i)
   {
      s << "Slot " << i << ": " << endl;
      List& l = myTable[i];
      KeyValuePair data;
      int c = 0;
   
      for (l.First(); l.GetCurrent(data); l.Next(), ++count, ++c)
      {
           s << "key = " << data.first << ", value = ";
          data.second.Dump(s);
           s << endl;
      }
   
      s << endl;
      min = (c < min) ? c : min;
      max = (c > max) ? c : max;
  }
  s << "Min = " << min << ", max = " << max << ", ave = " << count/myTableSize << endl;
  return s;
}

The Dump() function is used to dump the entire contents of the Array class to the supplied stream – this is used for debugging and analysis purposes (to see how many keys have hashed to the same slot in the table so that we can tell if our table is too big or too small).

Note also that both key and data are passed as const references so when they are inserted into the table a copy is made. You might like to think about the effect this design decision has when complex datatypes (like Colour) are inserted into the table. An alternative would be to store pointers or references rather than making a copy. This is easy enough, but then it raises the question of responsibility, i.e. who is responsible for managing elements in the table? Should the Array destructor also delete the Data if this was passed as a pointer? What happens if the caller who sent the data then goes and deletes it without telling Array so now the pointer to the deleted datatype is invalid in the HashTable? (Hint: a clever C++ solution to this problem is based on the idea of smart pointers or reference counted pointers).

The table stores a List which is used to handle collisions. For this we can used the List class developed for assignment 1. However, there is a problem: the List class from assignment 1 stores a string. The array needs to store both the key and the data in the list. For this we need to define a simple structure to store both types:

struct KeyValuePair { string first; Colour second; };

Actually, there is a standard library type, pair, that does the same thing. This uses templates... here is how you use the pair type:

#include <utility>
typedef pair<string, Colour > KeyValuePair;

We then define the DataType stored in the list as a KeyValuePair. Again, this could be simplified using templates, but changing the List class from assignment 1 is fine. Of course, the sorting function is no longer required and should be removed. No other changes to List are necessary.


Question 2

[20 marks]

You were asked to implement a colour lookup class and program that stores colours in the Array class developed in Question 1. For this we make Array a data member of class ColourLookUp (using private inheritance would also be acceptable).
ColourLookUp.h
#ifndef _COLOUR_LOOK_UP_
#define _COLOUR_LOOK_UP_
#include <fstream>
#include <string>
#include "AssocArray.h"
#include "Colour.h"
class ColourLookUp
{
   private:
   string importFileName;
   Array *hashTable;
   
   public:
   ColourLookUp(void)
   {
      hashTable = 0;
   }
   
   ColourLookUp(const string& newFileName);
   
   ~ColourLookUp(void)
   {
      // delete each of the colours in the lists
      // delete the lists
      // delete [] the hash table
   }
   // ColourLookUp::Lookup(string colourName), that returns the Colour
   // for the colour name if it exists, or black (0,0,0) otherwise.
   Colour Lookup(const KeyType& colourName)
   {
      return hashTable->Find(colourName);
   }
   
   ostream& Dump(ostream& s)
   {
      return (hashTable->Dump(s));
   }
};
#endif // _COLOUR_LOOK_UP_
ColourLookUp.cpp
#include "ColourLookUp.h"
ColourLookUp::ColourLookUp(const string& newFileName)
{
   Colour* newColourPtr;
   string dummyString, colourName;
   int redComponent=0, greenComponent=0, blueComponent=0;
   unsigned int hashTableSize = 0;
   
   ifstream inFile(newFileName.c_str());
   if (!inFile)
   {
       cerr << "\nERROR: Could not open file " << newFileName;
       exit(1);
   }
   
   // These lines utilise the first line of the file that was provided to store the colours...
inFile >> dummyString >> hashTableSize; if (hashTableSize <= 0) { cerr << "\nERROR: First element of file should be table size. Size=" << hashTableSize; exit(1); } // Now make the hash table hashTable = new Array(hashTableSize); // Read in all of the colours and put them in the table while (!inFile.eof()) { inFile >> colourName >> redComponent >> greenComponent >> blueComponent; newColourPtr = new Colour(redComponent, greenComponent, blueComponent); hashTable->Insert(colourName, *newColourPtr); } inFile.close(); }

ColourLookUp was made a public function – why?

Finally, let's look at main.cpp:

main.cpp
#include <string>
#include <iostream>
#include <fstream>
#include "Colour.h"
#include "List.h"
#include "AssocArray.h"
#include "ColourLookUp.h"
using namespace std;
int main (int argc, char * const argv[])
{
   ColourLookUp *myTable=0;
   string colourName, fileName;
   ifstream inFile;
   
   while (!inFile.is_open())
   {
      cout << "\nPlease enter a colour-list file name: ";
      cin >> fileName;
       inFile.open(fileName.c_str());
     
      if (!inFile)
      { cout << "\nSorry, could not open file: " << fileName; }
   }
   
   myTable = new ColourLookUp(fileName.c_str());
   cout << "\nLoaded colour list.";
   
   while (1)
   {
      cout << "\nPlease enter a colour name or 'q' to quit: ";
      cin >> colourName;
   
      if (colourName == "q") break;
      else (myTable->Lookup(colourName)).Dump(cout);
   }
   
   myTable->Dump(cout);
   delete myTable;
   return 0;
}

This simple test function reads from the standard input the colour, and prints the component values for the supplied input. This is repeated until "q" is entered.


This material is part of the CSE2305 - Object-Oriented Software Engineering course.
Copyright © Alan Dorin, 2006. All rights reserved.

Last Modified: September 18, 2006