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:

  • 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 in
  • isEmpty(): test whether is empty
  • entrySet(): return an iterable collection of entries in
  • keySet(): return an iterable collection of keys in
  • values(): return an iterator of the values in
Link to original

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 null

In 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 original
  • Hash Function

    A hash function has the properties:

    • Compression: maps an input of of arbitrary bit length to an output of fixed bit length which may be smaller
    • Polynomial time computable
    Link to original

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)
    Link to original
  • 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 .
    Link to original

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 null

      In 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
      Link to original
    • 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)
    Link to original

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:

  • 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
Link to original

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:

  • get(k): if the dictionary has an entry with key , return it otherwise null
  • getAll(k): returns an iterable collection of all entries with key
  • put(k,v): inserts into the dictionary and returns entry
  • remove(e): removes the entry from the dictionary and returns the removed entry; an error occurs if entry is not in the dictionary
  • entrySet(): returns an iterable collection of the entries in the dictionary
  • Additional methods: size(), isEmpty().
Link to original

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:

    • put takes time since we can insert the new item at the beginning or the end of the sequence
    • get and remove take 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:

    • get takes time using binary search
    • put takes time since in the worst case, we may have to shift items to make room for the new item
    • remove has the same time as put