## Chapter 8: Dictionaries

• The primary use of a dictionary is to store elements so that they can be located quickly using keys.
• Like a priority queue, a dictionary is a container of key-element pairs.
• Nevertheless, although a total order relation on the keys is always required for a priority queue, it is optional for a dictionary.
• Indeed, the simplest form of a dictionary assumes only that we can determine whether two keys are equal.
• When the total order relation on the keys is defined, then we can talk about an ordered dictionary, and we specify additional ADT functions that refer to the ordering of the keys.

### 8.1 The Dictionary Abstract Data Type

• A dictionary ADT stores key-element pairs (k,e) which we call items, where k is the key and e is the element.
• In an unordered dictionary we can use an equality tester object to test whether two keys, k1 and k2, are equal with function isEqualTo(k1, k2).
• The dictionary ADT models a searchable collection of key-element items
• The main operations of a dictionary are searching, inserting, and deleting items
• Multiple items with the same key are allowed
• Applications:
• credit card authorization
• mapping host names (e.g., www.nbu.bg) to internet addresses (e.g., 193.19.172.24)
• As an ADT, a dictionary D supports the following functions:
 Function Input Output Description size() - Integer Return the number of items in D. isEmpty() - Boolean Test whether D is empty. elements() - Iterator of objects (elements) Returns the elements stored in D. keys() - Iterator of objects (keys) Returns the keys stored in D. find(k) Object (key) Position If D contain an item with key equal to k, then return the position of such an item. If not, a null position is returned. findAll(k) Object (key) Iterator of Positions Return an iterator of positions for all items whose key equals k. insertItem(k,e) Objects k (key) and e (element) - Insert an item with element e and key k into D. removeElement(k) Object (key) - Remove an item with key equal to k from D. An error condition occurs if D has no such item. removeAllElements(k) Object (key) - Remove the items with key equal to k from D.
• Remarks:
• The way the items of a dictionary are stored is implementation dependent.
• The notation p(x) indicates the position of the item storing element x.
• Example:
 Operation Output Dictionary insertItem(5,A) insertItem(7,B) insertItem(2,C) insertItem(8,D) insertItem(2,E) find(7) find(4) find(2) findAll(2) size() removeElement(5) removeElement(5) removeAllElements(2) find(2) findAll(2) - - - - - p(B) "null" p(C) or p(E) p(C),p(E) 5 - "error" - "null" "empty iterator" {(5,A)} {(5,A),(7,B)} {(5,A),(7,B),(2,C)} {(5,A),(7,B),(2,C),(8,D)} {(5,A),(7,B),(2,C),(8,D),(2,E)} {(5,A),(7,B),(2,C),(8,D),(2,E)} {(5,A),(7,B),(2,C),(8,D),(2,E)} {(5,A),(7,B),(2,C),(8,D),(2,E)} {(5,A),(7,B),(2,C),(8,D),(2,E)} {(5,A),(7,B),(2,C),(8,D),(2,E)} {(7,B),(2,C),(8,D),(2,E)} {(7,B),(2,C),(8,D),(2,E)} {(7,B),(8,D)} {(7,B),(8,D)} {(7,B),(8,D)}
• Position class provides:
 Operation Input Output Description element() - Object (element) Return a reference to the element of the associated item. key() - Object (key) Return a constant reference to the key of the associated item. isNull() - Boolean Determine if this is a null position.

8.1.2 Log Files
• A simple way of realizing a dictionary is to use an unordered vector, list, or general sequence to store the key-element pairs.
• Such an implementation is called a log file.
• A log file is a dictionary implemented by means of an unsorted sequence
• We store the items of the dictionary in a sequence (based on a doubly-linked lists or a circular array), in arbitrary order
• Performance:
• insertItem takes O(1) time since we can insert the new item at the beginning or at the end of the sequence
• find and removeElement take O(n) time since in the worst case (the item is not found) we traverse the entire sequence to look for an item with the given key
• The log file is effective only for dictionaries of small size or for dictionaries on which insertions are the most common operations, while searches and removals are rarely performed (e.g., historical record of logins to a workstation)

### 8.2 Hash Tables

• One of the most efficient ways to implement a dictionary is to use a hash table.
• Although hash tables have high worst-case running times for dictionary ADT operations, we will see that their expected-case running time are excellent.
• Letting n denote the number of items, the worst-case running times are O(n), but the expected-case times are only O(1).
8.2.1 Bucket Arrays
• A bucked array for a hash table is an array A of size N, where each cell of A is thought of as a "bucket" (that is, a container of key-element pairs) and the integer N denotes the capacity of the array.
• If the keys are integers well distributed in the range [0, N - 1], this bucket array is all that is needed − an element e with a key k is simply inserted into the bucket A[k].
• If keys are not unique, then two different elements may be mapped to the same bucket in A. In this case, we say that a collision has occurs.

Data Structures and Algorithms in C++
Analysis of the Bucket Array Structure
• Achivеment: O(1) for all functions
• Drawback 1: It uses space Theta(N), wasteful when N is large relative to n
• Drawback 2: Keys are integers in [0, N - 1], which is often not the case
8.2.2 Hash Functions
• A hash function h maps keys of a given type to integers in a fixed interval [0, N − 1]
• Example: h(x) = x mod N is a hash function for integer keys
• The integer h(x) is called the hash value of key x
• A hash table for a given key type consists of
• Hash function h
• Array of size N
• When implementing a dictionary with a hash table, the goal is to store item (k, e) at index ih(k)
• Example:
 We design a hash table for a dictionary storing items (SSN, Name), where SSN (social security number) is a nine-digit positive integer Our hash table uses an array of size N = 10 000 and the hash function h(x) = last four digits of x To avoid any collision, we have to use N = 10 000 000 000 and the hash function h(x) = x (Drawback 1)

• A hash function is usually specified as the composition of two functions:
• Hash code map: h1: keys → integers
• Compression map: h2: integers → [0, N − 1]
• The hash code map is applied first, and the compression map is applied next on the result, i.e. h(x) = h2(h1(x))
• The goal of the hash function is to "disperse" the keys in an apparently random way
• The hash function is "good" if it maps the keys in out dictionary to minimize collisions as much as possible.
• Also it should be fast and easy to compute.
8.2.3 Hash Codes
• The integer assigned to a key k is called the hash code or hash value for k.
Hash Codes in C++
• We reinterpret the memory address of the key object as an integer
• Good in general, except for numeric and string keys
• Integer cast:
• We reinterpret the bits of the key as an integer
• Suitable for keys of length less than or equal to the number of bits of the integer type (e.g. char, short, int and float on many machines)
• Component sum:
• We partition the bits of the key into components of fixed length (e.g. 16 or 32 bits) and we sum the components, ignoring overflows
• Suitable for numeric keys of fixed length greater than or equal to the number of bits of the integer type (e.g. long and double on many machines)
A Small C++ Example
32-bit integer if we have 32-bit integer hash function
`int hashCode(int x){ return x; }`
64-bit integer if we have 32-bit integer hash function
`int hashCode(long x){  typedef unsigned long ulong;   return hashCode(static_cast<int>(static_cast<ulong>(x) >> 32)           + static_cast<int>(x));}`
Polynomial Hash Codes
• Polynomial accumulation:
• We partition the bits of the key into a sequence of components of fixed length (e.g., 8, 16 or 32 bits) a0 a1an−1
• We evaluate the polynomial
p(z) = a0 + a1z + a2 z2 + … + an−1zn−1
at a fixed value z, ignoring overflows
• Especially suitable for strings (e.g., the choice z = 33 gives at most 6 collisions on a set of 50 000 English words!)
• Polynomial p(z) can be evaluated in O(n) time using Horner’s rule:
• The following polynomials are successively computed, each from the previous one in O(1) time
p0(z) = an−1,  pi(z) = an−i−1 + zpi−1(z) (i = 1, 2, …, n − 1)
• We have p(z) = pn−1(z)
Cyclic Shift Hash Codes
`int hashCode(const char* p, int len) // hash a character array{ unsigned int h = 0;  for (int i = 0; i < len; i++)  { h = (h << 5)|(h >> 27);          // 5-bit cyclic shift    h += (unsigned int)p[i];         // add in next character  }  return hashCode(int(h));}`
Experimental Results
25000 English words
 Shift Collisions Total Collisions Max 0 23739 86 1 10517 21 5 4 2 6 6 2 11 453 4

Hashing Floating-Point Quantities
`int hashCode(const double& x)       // hash a double{ int len = sizeof(x);  const char* p = reinterpret_cast<const char *>(&x);  return hashCode(p, len);}`
C++ provides an operation called a reinterpret_cast, to cast between such unrelated types.
This cast treats quantities as a sequence of bits and makes no attempt to intelligently convert the meaning of one quantity to another.

hash_code.cpp

#### 8.2.4 Compression Maps

• The hash code for a key k will typically not be suitable for immediate use with a bucket array, because the range of possible hash codes for our keys will typically exceed the range of legal indices of our bucket array A.
The Division Method
• h2(y) = | y | mod N
• The size N of the hash table is usually chosen to be a prime number
• The reason has to do with number theory and is beyond the scope of this course
• h2(y) = |ay + b| mod N
• a and b are nonnegative integers such that a mod N ≠ 0
• Otherwise, every integer would map to the same value b
8.2.5 Collision-Handling Schemes
• Collisions occur when different elements are mapped to the same cell
Separate Chaining
• Chaining: let each cell in the table point to a linked list of elements that map there
• Chaining is simple, but requires additional memory outside the table

Open addressing: the colliding item is placed in a different cell of the table

Linear Probing
• Linear probing handles collisions by placing the colliding item in the next (circularly) available table cell
• Each table cell inspected is referred to as a “probe”
• Colliding items lump together, causing future collisions to cause a longer sequence of probes
• Example:
 h(x) = x mod 13 Insert keys 18, 41, 22, 44, 59, 32, 31, 73, in this order

Search with Linear Probing
 Consider a hash table A that uses linear probing find(k) We start at cell h(k) We probe consecutive locations until one of the following occurs: An item with key k is found, or An empty cell is found, or N cells have been unsuccessfully probed Algorithm find(k)    i ← h(k)    p ← 0    repeat       c ← A[i]       if c = ∅           return Position(null)       else if c.key() = k           return Position(c)       else          i ← (i + 1) mod N          p ← p + 1    until p = N    return Position(null)

• To handle insertions and deletions, we introduce a special object, called AVAILABLE, which replaces deleted elements
• removeElement(k)
• We search for an item with key k
• If such an item (k, e) is found, we replace it with the special item AVAILABLE and we return the position of this item
• Else, we return a null position
• insertItem(k, e)
• We throw an exception if the table is full
• We start at cell h(k)
• We probe consecutive cells until one of the following occurs:
• A cell i is found that is either empty or stores AVAILABLE, or
• N cells have been unsuccessfully probed
• We store item (k, e) in cell i
Double Hashing
• Double hashing uses a secondary hash function d(k) and handles collisions by placing an item in the first available cell of the series (i+ jd(k)) mod N for j = 0, 1, … , N −1
• The secondary hash function d(k) cannot have zero values
• The table size N must be a prime to allow probing of all the cells
• Common choice of compression map for the secondary hash function: d2(k) = q − k mod q where
• q < N
• q is a prime number
• The possible values for d2(k) are 1, 2, … , q
• Example:
 Consider a hash table storing integer keys that handles collision with double hashing N = 13 h(k) = k mod 13 d(k) = 7 − k mod 7 Insert keys 18, 41, 22, 44, 59, 32, 31, 73, in this order

#### 8.2.7 A C++ Hash Table Implementation

html-8.1 (HashEntry)
html-8.2
(Position)
html-8.3
(Hash1)
html-8.
4 (Hash2)

hash.cpp

### 8.3 Ordered Dictionaries

In an ordered dictionary, we wish to perform the usual dictionary operations, but also maintain an order relation for the keys in our dictionary.

An ordered dictionary supports the following functions beyond those included in the general dictionary ADT (8.1.1):
• closestBefore(k) - Return a position of an item with the largest key less than or equal to k.
• closestAfter(k) - Return a position of an item with the smallest key greater than or equal to k.
8.3.2 Look-Up Tables
• A lookup table is a dictionary implemented by means of a sorted sequence
• We store the items of the dictionary in an array-based sequence, sorted by key
• We use an external comparator for the keys
• Performance:
• find takes O(log n) time, using binary search
• insertItem takes O(n) time since in the worst case we have to shift n/2 items to make room for the new item
• removeElement takes O(n) time since in the worst case we have to shift n/2 items to compact the items after the removal
• The lookup table is effective only for dictionaries of small size or for dictionaries on which searches are the most common operations, while insertions and removals are rarely performed (e.g., credit card authorizations)
8.3.3 Binary Search
• Binary search performs operation find(k) on a dictionary implemented by means of an array-based sequence, sorted by key
• similar to the high-low game
• at each step, the number of candidate items is halved
• terminates after a logarithmic number of steps
• Example: find(7)

bsearch.cpp

Analysis of Binary Search

• The running time is proportional to the number k of recursive calls.
• The number of remaining candidates is reduced by at least one half with each recursive call.
• In the worst case (unsuccessful search), the recursive call stops when there are no more candidates, i.e. n/2k = 1, k = log n and we obtain O(log n) running time.
Comparing Simple Ordered Dictionary Implementations

 Function Log File Look-Up Table size(), isEmpty() O(1) O(1) keys(), elements() O(n) O(n) find(key) O(n) O(log n) findAll(key) Theta(n) O(log n + s) insertItem(key, element) O(1) O(n) removeElement(key) O(n) O(n) removeAllElements() Theta(n) O(n)