The Basics of Hash Table. Hash table is fundamentally a data... | by billydharmawan | Towards Data Science

Those of us who’ve lived reasonably long enough must know or at least have seen a yellow page. Yup! You’re right. It is the thick yellow book containing directories of businesses and their phone numbers. This enables us to search for a business that sells what we need and contact them.
(I don’t think Gen Z and the next generations after that would come to know about this thick yellow book. 😆)
A directory of phone number is usually alphabetically ordered, so we know where we can start our search. Once we find the business name that we want, we can grab the phone number and call them. You get the idea. 💡
What if I tell you that a yellow page or a phone book is an implementation of hash table? You bet! 🐴
Hash table is essentially an array coupled with a hash function. It is commonly used to store key-value data, for example, businesses and their phone number, students and their grades, items and their prices and many more, in an unordered fashion.
Each key has to be unique and is mapped to a specific index in the array and its value stored in that location. 🔑 ➡🚪 Notice that because of this, insertion, search and delete operations are ⚡️ fast. In fact, the average time complexity of insertion, search and delete operations of hash table is constant time or O(1).
Therefore, when you require a data structure that offers fast insertion, lookup and deletion, hash table is one of the go-to option. This can be really useful when you have a large amount of relational key-value data, e.g. in Data Science and/or Machine Learning algorithm.
Hash function is what makes hash table a powerful and useful data structure. A hash function takes a piece of data, or usually referred to as a key, and returns a hash code as an output. This hash code, which is an integer, is then mapped to an index in the array, where the value will be stored.
One of the reasons why the hash code is not associated directly with an index in the array is because the value of the hash code can be very large, e.g. 10000000, whilst the amount of key-value data we want to store (or the size of the array that forms our hash table) might not necessarily be that big.
A simple way to map the hash code to an index in the array is by applying a modulo operation with respect to the size of the hash table.
index = hashCode(String key) % length(array)
Let’s take a look at an example of a hash function.
function int hashCode(String s) {  return s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]}
This is the hash function used by Java String object, where s is the array of characters of the String object, e.g. s[0] is the first character, and n is the length of the String object. Let’s see it in action.
hashCode("Apple") // 63476538hashCode("Costco") // 2024204569hashCode("Ikea") // 2280798hashCode("Snowflake") // 1973786418
Notice from the few examples ☝️ that the hash function outputs different hash code values for each String object. As mentioned earlier, each key is expected to be unique, so the hash function is also expected to produce unique hash code for each key. This will allow a better chance for the key-value data to be uniformly distributed across the hash table.
Furthermore, let’s map the hash codes to the indices in the array.
function int mapToIndex(int hashCode, int arraySize) {  return abs(hashCode % arraySize)}// Let's assume the size of our hash table or the length of the array is 500hashTable = Array(500)mapToIndex(hashCode("Apple"), size(hashTable)) // 38mapToIndex(hashCode("Costco"), size(hashTable)) // 69mapToIndex(hashCode("Ikea"), size(hashTable)) // 298mapToIndex(hashCode("Snowflake"), size(hashTable)) // 418
As per ☝️ Apple’s data will be located at index 38, Costco’s data at index 69, Ikea’s data at index 298 and Snowflake’s data at index 418.
In the previous section, we had a look at an example of how hash function is used to determine the location of a particular key-value data in the hash table. Now, let’s take a look at another example of a hash function.
function int hashCode(int i) {  return i % 5}
This hash function takes an integer as input and apply modulo operation to output the hash code. Now, let’s test it out with a few integer input.
hashCode(1) // 1hashCode(5) // 0hashCode(11) // 1
Wait a second. Isn’t hash code supposed to be different for each key? Well, in this case, the hash function we chose is probably not ideal for the possible keys that we might have. It produces the same hash code for different input values, i.e. input values of 1 and 11 will return a hash code of 1, which means they will be mapped to the same location in the array. This phenomenon is called collision.
There are two common ways used to deal with collisions in hash table:
linear probing
separate chaining
In linear probing, we deal with collisions in hash tables by searching for the nearest available space in the array after the supposed location as determined by the hash function. Let’s imagine this using the collision example from the previous section, where input values of 1 and 11 result in the same hash code.
Let’s say we insert key-value data of 1-Apple into our hash table. The hash function returns hash code of 1, and let’s assume it is mapped to index 1 in the array. Now, index 1 in the array contains key-value data of 1-Apple.
Next, we want to add key-value data of 11-Orange into the hash table. The hash function returns hash code of 1, which is also mapped to index 1, however, by this time, the block in index 1 of the array has already been occupied by key-value data 1-Apple.
In this case, linear probing will look for the closest free location in the array and store the key-value data 11-Orange there. The same is also true when we want to retrieve data from the hash table where the key equals 11. Linear probing will find key-value data 1-Apple first, then keeps searching in the adjacent locations until it finds a block in the array that matches the key 11 or an empty block, which indicates the key 11 does not exist.
When separate chaining method is used, hash table will store key-value data in linked lists. This means that each block in the array contains a linked list, not just a single key-value data.
So, at the time of collision, instead of looking for the next free block in the array like what linear probing does, separate chaining adds the key-value data into the linked list.
Again, taking the collision example from the previous section, separate chaining will store both 1-Apple and 11-Orange key-value pairs in a linked list at index 1 of the array, where the head of the linked list would be 1-Apple as it was added before 11-Orange.
Based on what we have learnt so far, we can deduce that a good hash function should have the following properties.
A good hash function should make use of every element of the data in order to increase the number of possible hash codes.
Let’s take a look at the following example.
function int hashCode(String k) {  String s = k.substring(start=0, end=2)  return s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]}
This hash function accepts an input data, which has String type, takes the first 3 characters and apply the hash function used by Java String object. You can imagine that collisions will occur for data that have the same first three characters, e.g. apple and application.
Not all piece of information are used to generate the hash code in this case. As you can see, there’s a higher chance of collision because there’s less available possible hash codes.
One of the reasons why we would use hash table is its speed of insertion, lookup and deletion. Every operation relies on the hash function to get the hash code and hence, the position of data in the array.
So, we want to make sure that our hash function is super fast in order to provide the speed that we are looking for in hash table.
This means we should avoid hash function that uses complex or slow operation and aim for speedy, simple and yet, effective operation.
It is most ideal for our hash function to uniformly distribute the data across the hash table. If the hash function does not distribute the data evenly, then there will be a higher chance for a collision to occur.
We have learned the basics of hash table, which include its structure, what hash function is and does, collisions and ways to deal with them. The next time you are faced a problem that requires speedy lookup, insertion and deletion, hash table is probably your best bet to solve it. 😺