Week 8. Maps and Dictionaries
Map
Map
A map models a searchable collection of key-value entries. The main operations consist of searching, inserting and deleting items. Multiple entries with the same key are not allowed.
Link to original
Map ADT
Map (ADT)
The Map ADT consists of the methods:
Link to original
get(k): if map has an entry , return ; else null.put(k,v): if does not have an entry then add it to the map and return null; otherwise, replace with the new value and return the old value.remove(k): if the map has entry with key , remove it from and return associated value otherwise return null.size(): return number of entries inisEmpty(): test whether is emptyentrySet(): return an iterable collection of entries inkeySet(): return an iterable collection of keys invalues(): return an iterator of the values in
List-based Map Implementation
We can implement a map using an unsorted list. We store the items of the map in a double-linked list .

Pseudo-code implementations of common operations:
def get(k):
B = S.positions() # iterator over positions
while B.hasNext():
p = B.next()
if p.element().getKey() == k:
return p.element().getValue()
return null
def put(k,v):
B = S.positions()
while B.hasNext():
p = B.next()
if p.element().getKey() == k:
# if it's in map, return and replace
t = p.element().getValue()
p.element().setValue(v)
return t
# if key is not in map, append
S.append((k,v))
n += 1 # increment no. of entries
return null
def remove(k):
B = S.positions()
while B.hasNext():
p = B.next()
if p.element().getKey() == k:
t = p.element().getValue()
S.remove(p)
n -= 1
return t
return nullIn terms of performance, all of these methods take time since in the worst case, the item is not found and we traverse the entire list to look for an item with the given key. This implementation is only effective for maps of small size.
Hash Table
Hash Table
Hash Tables are used to implement a Map, they consist of two main components:
Bucket Array
Bucket Array: array of size where each cell of is thought of as a “bucket” (collection of key-value pairs)
Link to originalHash Function
A hash function has the properties:
Link to original
- Compression: maps an input of of arbitrary bit length to an output of fixed bit length which may be smaller
- Polynomial time computable
When implementing a map with a hash table, we store the item at the index .
Link to original
Hash Functions
A Hash Function is usually specified as the composition of two functions:
Hash Code
Hash Code : There are quite a few ways we can convert keys:
- Memory address: we reinterpret the memory address of the key object as an integer (default hash code for Java objects)
- Integer cast: we reinterpret the bits of the key as an integer
- Component sum: we partition the bits of the key into fixed length components, and sum the components (ignoring overflow)
Compression Function
Compression function : We need to ensure the probability of two different keys getting hashed to the same bucket is . We can use:
- Division: where is usually chosen to be a prime
- Multiply, Add and Divide: Where is the size of the bucket array, is a prime number greater than , and / are integers chosen at random from the interval with .
The hash function applies the hash code function then the compression function; .
Collision Handling
Collisions occur when different elements are mapped to the same bucket, there are different ways we can handle collisions:
Separate Chaining
Separate Chaining: let each cell in the table point to a linked list of entries. This is simple but required additional memory outside of the table.
Link to original
Open Addressing
Open addressing: the colliding item is placed in a different cell of the table, each table cell inspected is referred to as a “probe”. Colliding items lump together, causing future collisions to cause a longer sequence of probes. There are three different ways we can handle this:
Linear Probing
Linear Probing: handles collisions by placing the colliding item in the next (circularly) available table cell. If we try to insert a key into a cell that is already occupied, we try again at (and repeat until we find an empty cell to insert into).
In order to
get(k):- We start at cell
- Probe consecutive locations until one of the following occurs:
- item with key is found
- empty cell is found
- cells have been unsuccessfully probed
Example pseudo-code:
def get(k): i = h(k) p = 0 repeat: c = A[i] if c == null: return null else if c.getKey() == k: return c.getValue() else: i = (i + 1) mod N p += 1 until p == N return nullIn order to
remove(k):- search for an entry with key
- if such an entry is found, replace it with special item “available” and we return element
- else, return
In order to
put(k,v):- throw an exception if the table is full
- start at cell
- probe consecutive cells until:
- a cell is found that is either empty or stores “available”
- or until cells have been unsuccessfully probed
- we store in cell
Double Hashing
Double Hashing: we use a secondary hash function . If maps some key to a cell with , that is already occupied, then we iteratively try the buckets:
Where .
The secondary hash function cannot have zero values. The table size must be prime to allow probing of all of the cells.
A common choice of compression function for the secondary hash function:
Where and is prime.
Link to original- Quadratic Hashing: (not incl., p.396, individual study)
Hash Performance
In the worst case: searches, insertions and removals on a hash table take time. The worst case occurs when all keys inserted into the map collide.
Load Factor (Hashing)
The load factor affects the performance of a hash table. Assuming that the hash values are like random numbers, it can be shown that the expected number of probes for an insertion with open addressing is .
Link to original
Rehashing (Hashing)
To keep hashing performance up, we need to keep the load factor low. We use rehashing to do so:
Link to original
- whenever the load factor becomes too large (e.g. greater than 0.5):
- create a new bucket approx. double the size of the old one
- iterate all elements in old bucket array and put them in the new bucket
Dictionary
Dictionary
A dictionary models a searchable collection of key-value entries. The main operations of a dictionary are searching, inserting and deleting items. Multiple items with the same key are allowed.
Link to original
Dictionary ADT
Dictionary (ADT)
A dictionary typically implements the methods:
Link to original
get(k): if the dictionary has an entry with key , return it otherwise nullgetAll(k): returns an iterable collection of all entries with keyput(k,v): inserts into the dictionary and returns entryremove(e): removes the entry from the dictionary and returns the removed entry; an error occurs if entry is not in the dictionaryentrySet(): returns an iterable collection of the entries in the dictionary- Additional methods:
size(),isEmpty().
Dictionary Implementations
There are different ways we can implement a dictionary:
-
List-Based Dictionary: usually a log file or audit trail is a dictionary backed by an unsorted sequence; we store the items in an array or doubly-linked list in any arbitrary order. Performance:
puttakes time since we can insert the new item at the beginning or the end of the sequencegetandremovetake time since in the worst case, we traverse the entire sequence to look for an item with the given key
-
Hash Table Implementation: we can also create a hash-table backed dictionary; if we used separate chaining to handle collision: then each operation can be delegated to a list-based dictionary stored at each hash table cell
-
Ordered Search Table: we can also choose to back the dictionary using a sorted / ordered array; we store the items of the dictionary in an array-based sequence sorted by the key.
A search table is only effective for dictionaries of small size or where searches are the most common operations while changes are uncommon.
Performance:
gettakes time using binary searchputtakes time since in the worst case, we may have to shift items to make room for the new itemremovehas the same time asput