10Data Structures---Hash Tables

The course is mainly about Hash Tables.Generally covered More applications of maps,Compression functions,Hash table operations,Collision avoidance,Open addressing,Complexities of insert, delete, search and so on.

1.Hash Tables A map (also called dictionary) is an ADT that contains key-value pairs. It helps retrieve the value using the key as the “address.” As an example, consider a set of two-letter words. You want to build a dictionary so that you can look up the definition of any two-letter word, very quickly. The two- letter word is the key that addresses the definition of the word. Key Definition am First  person  singular  number  indicative  of  be,  also,  before  noon   an The  form  of  ‘a’  before  a  vowel  sound   …   be To  exist  or  to  live   by (Preposition)  near  or  next  to     …

2.More applications of maps (University record) Key = student id. Value = information about the student (Social media) Key = user name / email id. Value = user information. So, how will you implement a dictionary of all two-letter words? Take an array with 26x26= 676 elements. Each two-letter will map to a unique index of the array. The content of that array index will be the definition. Is this scalable? NO. Consider large words … Hash tables implement dictionaries Number of keys = n Array size = N, and The number of possible keys M >> N > n

3.A hash function maps a huge set of keys into N buckets by applying a compression function, called a hash function h h (key) = array index (in the  dictionary).   Set hash compression bucket of code function array key integer keys index hash function Maps or dictionaries are associative arrays, where searching is based on the key, rather than an index to the array storing the values.

4. (Source: Wikipedia) Hash codes key K h(key) Java relies on 32-bit hash codes, so for the base types byte, short, int, char, we can get a hash code by casting its value x0  x1  x2  ... xn−2  xn−1  to the type int, and using the integer representation.

5.For a string object s = s[0] s[1] … s[n-1], the following method is used to compute the hash code h(s): h(s) = s[0]*31^(n-1)+s[1]*31^(n-2)+ ... + s[n-1] This is called polynomial hash code. When using a hash table to implement a map, for consistency, equivalent keys must map to the same bucket. So, if x.equals(y) then x.hashCode == y.hashCode Compression functions Let us get back to the dictionary of all 2-letter words. There are 26 x 26 = 676 possible keys, but perhaps 70 possible meaningful words. Let us convert each 2-letter word xy to an integer i as follows (using f(a) = 0, f(b) = 1, f(c) = 2, …, f(z) = 25): i = 26.f(x) + f(y)

6.Division method. If we plan on storing these words in an array of size 100 (i.e. N=100) then a simple compression function is mod 100 (if N = bucket array size, then use mod N). Thus, the integer i will be placed in location i mod N of the hash table MAD (Multiply-Add-Divide) method Map i to [(a.i + b) mod p] mod N Here p is a prime number and a, b are random integers chosen from the interval [0..p-1] Depending on the value of n/N, it works in most cases, but what if there are two words xy and pq such that h(xy) = h(pq)? This is called collision. Computing the compression function for collision avoidance is a form of black art. Use a function so that two different keys do not map to the same index of the hash table.

7.Hash table operations A hash table supports at least three operations: insert(key, value) Compute the key's hash code. Compress it to determine the entry's bucket. Insert the entry (key and value together) into that bucket (and deal with collision) find(key) Hash the key to determine its bucket. Search the entry with the given key. If found, return the entry, else, return null. delete(key) Hash the key to determine its bucket. Search the list for an entry with the given key. Remove it from the list if found. Return the entry or null if not found.

8.Collision avoidance Hashing with separate chaining (Also called Chain hashing) Each bucket entry references a linked list of entries, called a chain. If several keys are mapped to the same bucket, their definitions all reside in that bucket's linked list. 0 1 2 3 4 5 6 7 8 9 10 But how do we know which definition corresponds to which word? The answer is that we must store each key in the table along with its definition (i.e. both key and value).

9.Open addressing Linear Probing i hash Key Let i be the hash function of an entry X, but assume that slot i of the hash table is not free, since it has been allocated to entry Y (so that is a collision). Try looking for slots i+1, i+2, i+3, … until a free slot is found. Quadratic Probing In case of a collision at slot i, try looking for slots i+12, i+22, i+32, … until a free slot is found.

10.Typically we expect O(1) time performance for each of the operations. This may not be feasible if the load factor n/N is large (> 0.75) or there are too many collisions. The performance can slowly degenerate towards O(n). Resizing the hash table It is not always possible to foresee the number of entries we'll need to store. So, what to do when the load factor increases? One option is to enlarge the hash table when the load factor becomes too large. Allocate a new array (typically at least twice as long as the old), and then walk through all the entries in the old array and “rehash” them into the new. [Note: you  just  can’t  copy  the  entries  of  the  old  array  into   the   slots   of   the   new   array,   because   the   compression   functions  of  the  two  arrays  will  be  different.    You  have  to   rehash  each  entry  individually.] For best performance, hash codes often need to be designed specially for each new object.

11.Complexities of insert, delete, search Initially all empty buckets contain “null”. To insert a key K, first find if it K already exists. If so, the new value will replace the old value. Otherwise insert it into a blank slot. To delete a key, first find it, and then replace it by null. Case 1. Chain hashing Needs additional space. Each bucket has to maintain an independent linked list. The lengths of these lists should be as small as possible otherwise search complexity will increase. If the space is tight, this can scale up to O(n).

12.Case 2. Open addressing with linear probing 6 10 Y Z W T X Key X index 6 6 10 Y 10 X Key X As n/N increases, insert and search takes more time. What happens when you use open addressing and delete the key Y (see figure above)? Will it erase the link 10 also? No. So, to delete a key, simply mark it as “defunct” that will preserve the link. A new key can be inserted into the defunct slot only if it does not exist at all (for this look beyond the defunct entry).

13.Computing the hashCode of a given key should be fast, while minimizing the probability of collision. For a string s, the hash is computed as follows: int hash = 0; for (int i = 0; i < s.length(); i++) hash = (31 * hash + s.charAt(i)) % N Why can it be computed fast? Because it avoids the use of a multiplier (which is slow)! How? Horner’s  Rule  for  computing  polynomials   h(s) = s[0]*31^(n-1)+s[1]*31^(n-2)+ ... + s[n-1]   y0  =  s[0]     y1  =  31*y0  +  s[1]   y2  =  31*y1  +  s[2]   y3  =  31*y2  +  s[3]   …   …   …   h(s)  =  yn-­‐1  =  31*yn-­‐2  +  s[n-­‐1]    

14.Example. Consider the following keys to be entered into a hash table, first into a table of size 5. “Tom”, “Dick”, “Harry”, “Sam”, “Pete” Key hashCode() hashCode()%5 Tom 84274 4 Dick 2129869 4 Harry 69496448 3 Sam 82879 4 Pete 2484038 3 The load factor is 100%. Try with a larger table of size 11

15.Tidbits If   you   use   hashCode()%N   as   compression   function,   then   note   that   it   may   sometimes   return   a   negative   value   (when   the   argument   is   negative)   leading   to   an   array  out-­‐of-­‐bound  exception.  Add  N  to  make  it  +ve.     Also,   the   function   Math.abs(s.hashCode()%N   can   return  a  negative  integer  when  the  32-­‐bit  argument  is   10000000000…00,   since   its   positive   version   cannot   be   represented  using  32  bits  (needs  33  bits)!     One  way  out  is  the  following     private int hash(Key key) { return (key.hashCode() & 0x7fffffff) % M; }     It  masks  the  sign  bit  and  converts  it  into  a  positive   integer.    

16.Quadratic   probing   is   slow   since   it   involves   multiplication.   Here   is   how   you   can   speed   it   up   to   calculate  the  next  index.     Initially  k=  -­‐1   k  =  k+2   index  =  (index  +  k)  %  hashTableSize     Why  does  it  work?