Hash Table

All about Hash Tables


A hash table is a data structure that converts a value into a "hash" key and stores it into an array, allowing for fast access to values based on keys.

One of the good features of a Hash Table is that various methods runs in amortized constant time (e.g. search). Other methods like insertion or deletion runs in constant time.

Although this data structure is not recommended nowadays since there are better solutions (hashmap) or already built-in in programming languges, it is worth learning how it works unders the hood.

Hashing

The hashing method can accept a string, integer, or float number. It will return a integer that will represent the index of the array to be stored.

function hashCode(string) {
  const R = 31 // prime
  let hash = 0
  for (let i=0; i < string.length; i++) {
    hash = (R * hash + (+string[i])) % M
  }
  return Math.floor(hash)
}

Collision Resolution

Given that the number of keys to store (e.g. individual ATM transactions over the entire state of Maryland for a large bank organization) is enormous and the available space to store them in computer memory is much smaller, collisions are inevitable, even with an excellent hash function.

Then it is important to develop collision resolution methods, whose job is to determine how an insertion of a key that collides with an existing key is resolved. There are 4 ways to resolve them.

Separate Chaining

In the image, we're using a linked list because in that way we can store collided keys.

Note that it is not necessary that we employ a linked list for implementing the collision "chains". We could just as well use an AVL Tree, a Red-Black or B-Tree, or a SkipList!

The benefit of using a linked list for our collision chains is that we can insert very fast (by adding to the front or by adding to the back with a tail pointer).

The drawback is that we have linear time for search, but with MM relatively large and a good hash function, we are hoping that the collision chains will, on average, have length nM\frac{n}{M}, which is still linear time but offers a favorable constant of 1M\frac{1}{M}.

class SeparateChainingHashTable implements HashTable {
  private table: KVPairList[]; // array to store hashed keys
  private size: number;
  private primeGenerator: PrimeGenerator;
 
  private hash(key: string): number {
    // mask the first bit to filter away negative values.
    return (key.hashCode() & 0x7fffffff) % table.length;
  }
 
  constructor () {
    this.count = 0;
    this.primeGenerator = new PrimeGenerator();
    // intialize internal storage
    this.table = Array(primeGenerator.getCurrPrime()).fill(new KVPairList())
  }
 
  public put(key: string, value: string): Probes {
    // add the key value pair to the linked list
    this.table[this.hash(key)].addBack(key, value);
    size++;
    // The `Probes` object tracks the number of accesses to the `table` array.
    return new Probes(value, 1);
  }
 
  public get(key: string): Probes {
    return this.table[this.hash(key)].getValue(key);
  }
 
  public remove(key: string): Probes {
    // remove the key value pair from the list
    let probe = this.table[this.hash(key)].removeByKey(key);
 
    // if the value is not null, then the key is found
    if (probe.getValue() != null) {
      size--;
    }
    return probe;
  }
 
  public containsKey(key: string): boolean {
    return this.table[this.hash(key)].containsKey(key);
  }
 
  public containsValue(value: string): boolean {
    for (let i = 0; i < this.table.length; i++) {
      if (this.table[i].containsValue(value)) {
        return true;
      }
    }
    return false;
  }
 
  public size(): number{
    return this.size;
  }
 
  public capacity(): number{
    return table.length;
  }
 
 
  // Enlarges this hash table. At the very minimum, 
  // this method should increase the capacity of the hash table 
  // and ensure that the new size is prime.
  public enlarge(): void {
    let copy: KVPair[];
 
    // Copy all the elements in the table to the list
    for (let i = 0; i < this.table.length; i++) {
      for (let pair of this.table[i]) {
        copy.add(pair);
      }
    }
 
    // create a new table with the next prime
    this.table = new KVPairList[primeGenerator.getNextPrime()];
    this.size = 0;
 
    // initialize the new table
    for (int i = 0; i < this.table.length; i++) {
      this.table = Array(this.table.length).fill(new KVPairList())
    }
 
    // put all the elements in the list to the new table
    for (let pair of copy) {
      this.put(pair.getKey(), pair.getValue());
    }
  }
 
   // Shrinks this hash table. At the very minimum, 
   // this method should decrease the size of the hash table 
   // and ensure that the new size is prime.
  public shrink(): void {
    let copy: KVPair[];
 
    // Copy all the elements 
    for (let i = 0; i < this.table.length; i++) {
      for (let pair of this.table[i]) {
        copy.add(pair);
      }
    }
 
    // create a new table with the previous prime
    this.table = new Array(primeGenerator.getNextPrime())
    this.size = 0;
 
    // initialize at each index of the new table
    for (let i = 0; i < this.table.length; i++) {
      this.table[i] = new KVPairList();
    }
 
    // put all the elements in the copy list to the new table
    for (let pair of copy) {
      this.put(pair.getKey(), pair.getValue());
    }
  }
}

Linear Probing

Ordered Linear Probing

Quadratic Probing