CSE2305 - Object-Oriented Software
Engineering
Assessment
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. 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 |
#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 |
// 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. // 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.
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 |
#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 |
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... |
ColourLookUp
was made a public function – why?
Finally, let's look at main.cpp:
main.cpp |
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.
Last Modified: September 18, 2006