Hash Table is a data structure which organizes data using hash functions in order to support quick insertion and search.

There are two different kinds of hash tables: hash set and hash map.

  • The hash set is one of the implementations of a set data structure to store no repeated values.
  • The hash map is one of the implementations of a map data structure to store (key, value) pairs.

It is easy to use a hash table with the help of standard template libraries. Most common languages such as Java, C++ and Python support both hash set and hash map.

By choosing a proper hash function, the hash table can achieve wonderful performance in both insertion and search.

In this card, we will answer the following questions:

  1. What is the principle of a hash table?
  2. How to design a hash table?
  3. How to use hash set to solve duplicates related problems?
  4. How to use hash map to aggregate information by key?
  5. How to design a proper key when using a hash table?

And we also provide exercises for you to be familiar with hash table.

Design a Hash Table

In this chapter, we will discuss the underlying principle of the hash table.

After completing this chapter, you should be able to answer the following questions:

  1. What is the principle of hash table?
  2. Which factors will influence the choice of hash function and collision resolution strategy?
  3. Understand the difference between a hash set and a hash map.
  4. How to design a simple version of hash set and a hash map as in a typical standard template library.
  5. What is the complexity of insertion and lookup operations?

The Principle of Hash Table

As we mentioned in the introduction, Hash Table is a data structure which organizes data using hash functions in order to support quick insertion and search. In this article, we will take a look at the principle of the hash table.

The Principle of Hash Table

The key idea of Hash Table is to use a hash function to map keys to buckets. To be more specific,

  1. When we insert a new key, the hash function will decide which bucket the key should be assigned and the key will be stored in the corresponding bucket;
  2. When we want to search for a key, the hash table will use the same hash function to find the corresponding bucket and search only in the specific bucket.

An Example

HashTable

In the example, we use y = x % 5 as our hash function. Let's go through the insertion and search strategies using this example:

  1. Insertion: we parse the keys through the hash function to map them into the corresponding bucket.
    • e.g. 1987 is assigned to bucket 2 while 24 is assigned to bucket 4.
  2. Search: we parse the keys through the same hash function and search only in the specific bucket.
    • e.g. if we search for 1987, we will use the same hash function to map 1987 to 2. So we search in bucket 2 and we successfully find out 1987 in that bucket.
    • e.g. if we search for 23, will map 23 to 3 and search in bucket 3. And We find out that 23 is not in bucket 3 which means 23 is not in the hash table.

Keys to Design a Hash Table

There are two essential factors that you should pay attention to when you are going to design a hash table.

1. Hash Function

The hash function is the most important component of a hash table which is used to map the key to a specific bucket. In the example in previous article, we use y = x % 5 as a hash function, where x is the key value and y is the index of the assigned bucket.

The hash function will depend on the range of key values and the number of buckets.

Here are some examples of hash functions:

example

It is an open problem to design a hash function. The idea is to try to assign the key to the bucket as uniform as you can. Ideally, a perfect hash function will be a one-one mapping between the key and the bucket. However, in most cases a hash function is not perfect and it is a tradeoff between the amount of buckets and the capacity of a bucket.

2. Collision Resolution

Ideally, if our hash function is a perfect one-one mapping, we will not need to handle collisions. Unfortunately, in most cases, collisions are almost inevitable. For instance, in our previous hash function (y = x % 5), both 1987 and 2 are assigned to bucket 2. That is a collision.

A collision resolution algorithm should solve the following questions:

  1. How to organize the values in the same bucket?
  2. What if too many values are assigned to the same bucket?
  3. How to search a target value in a specific bucket?

These questions are related to the capacity of the bucket and the number of keys which might be mapped into the same bucket according to our hash function.

Let's assume that the bucket, which holds the maximum number of keys, has NN keys.

Typically, if NN is constant and small, we can simply use an array to store keys in the same bucket. If NN is variable or large, we might need to use height-balanced binary search tree instead.

Exercise

By now, you should be able to implement a basic hash table. We provide the exercise for you to implement a hash set and a hash map. Read the requirement, determine your hash function and solve the collision if needed.

If you are not familiar with the concepts of hash set and hash map, you can go back to the introduction part to find out the answer.

Insertion and search are two basic operations in a hash table.

Besides, there are operations which are based on these two operations. For example, when we remove an element, we will first search the element and then remove the element from the corresponding position if the element exists.

Question - Design HashSet

Design a HashSet without using any built-in hash table libraries.

To be specific, your design should include these functions:

1
2
3
add(value): Insert a value into the HashSet. 
contains(value) : Return whether the value exists in the HashSet or not.
remove(value): Remove a value in the HashSet. If the value does not exist in the HashSet, do nothing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import java.util.ArrayList;
import java.util.List;

class MyHashSet {
private final int MAX_LEN = 100000; // the amount of buckets
private List<Integer>[] set; // hash set implemented by array

/**
* Returns the corresponding bucket index.
*/
private int getIndex(int key) {
return key % MAX_LEN;
}

/**
* Search the key in a specific bucket. Returns -1 if the key does not existed.
*/
private int getPos(int key, int index) {
// Each bucket contains a list.
List<Integer> temp = set[index];
if (temp == null) {
return -1;
}
// Iterate all the elements in the bucket to find the target key.
for (int i = 0; i < temp.size(); ++i) {
if (temp.get(i) == key) {
return i;
}
}
return -1;
}

/**
* Initialize your data structure here.
*/
public MyHashSet() {
set = new ArrayList[MAX_LEN];
}

public void add(int key) {
int index = getIndex(key);
int pos = getPos(key, index);
if (pos < 0) {
// Add new key if key does not exist.
if (set[index] == null) {
set[index] = new ArrayList<Integer>();
}
set[index].add(key);
}
}

public void remove(int key) {
int index = getIndex(key);
int pos = getPos(key, index);
if (pos >= 0) {
// Remove the key if key exists.
set[index].remove(pos);
}
}

/**
* Returns true if this set did not already contain the specified element
*/
public boolean contains(int key) {
int index = getIndex(key);
int pos = getPos(key, index);
return pos >= 0;
}
}

/**
* Your MyHashSet object will be instantiated and called as such:
* MyHashSet obj = new MyHashSet();
* obj.add(key);
* obj.remove(key);
* boolean param_3 = obj.contains(key);
*/