# Hash Tables

- Author: Luiz Schons
- Published: 2024-03-06
- URL: https://luizschons.com/posts/hash-tables/
- Português: https://luizschons.com/pt-br/posts/tabelas-hash.md
- Topics: Algorithms, Data structures, Go

A hash table is a data structure that maps lookup keys to values.

Its main job is to take a simple key and find the value you want quickly and efficiently.

### 1. Hash Functions

### 1.1. Introduction

A hash function takes some input data and produces a number that identifies the position of an element in a hash table.

We have already looked at other data structures on this blog, such as finding an element in a linked list or in a binary tree, and we saw that those operations take O(n) and O(log n), respectively.

A hash table, on the other hand, can do that lookup in O(1), that is, in constant time.

This means that no matter how large the table is, the lookup takes the same time.

### 1.2. Example

A hash function is often used to create an index into an array, which is why it needs to be fast and efficient.

I wrote a very simple hash function that takes a word and returns a number that represents it.

```go
package main

import (
    "fmt"
)

func main() {
    for {
        run()
    }
}

func run(){
    words := [10000]string{}
    fmt.Println("Type a word to hash")
    var word string
    fmt.Scanln(&word)
    wordHash := fake_hash(word)
    fmt.Println("The word", word, "has the hash", wordHash)
    words[wordHash] = word
    fmt.Println("The word", word, "was added to the array at position", wordHash)
    fmt.Println("Type a hash to look up the matching word")
    var hash int
    fmt.Scanln(&hash)
    fmt.Println("The word for hash", hash, "is", words[hash])
}

func fake_hash(word string) int {
    hash := 0
    for i := 0; i < len(word); i++ {
        hash += int(word[i])
    }
    return hash
}
```

In this example, the fake_hash function takes the word and adds up the ASCII values of its characters, returning a number that represents the word.

But don't worry: you probably won't need to write a hash function yourself, since programming languages already ship with optimized ones.

Your language may call it something else, like a “map” or a “dictionary”, but the concept is the same.

### 2. Use cases

### 2.1. Phone Book

A classic example of a hash table is a phone book.

Imagine you want to find someone's phone number.

If the phone book were a linked list, you would have to walk the whole list until you found the person's name.

If it were a binary tree, you would have to walk down the tree until you found the name.

But since the phone book is a hash table, you can find a person's number in constant time, that is, in O(1).

Let's understand this better.

When you add a name and a phone number to the phone book, the hash function is used to generate an index for that name.

When you want to find someone's number, the hash function generates the index for that name, and the phone number is returned.

Like this:

```go
package main

import (
    "fmt"
)
func main() {
    phoneBook := make(map[string]string)
    phoneBook["John"] = "1234-5678"
    phoneBook["Mary"] = "8765-4321"
    phoneBook["Joseph"] = "4321-5678"
    phoneBook["Anna"] = "5678-4321"
    fmt.Println("John's phone number is", phoneBook["John"])
    fmt.Println("Mary's phone number is", phoneBook["Mary"])
    fmt.Println("Joseph's phone number is", phoneBook["Joseph"])
    fmt.Println("Anna's phone number is", phoneBook["Anna"])
}
```

In this example, the hash function generates an index for each name, and the phone number is returned in constant time.

### 2.2. DNS

Another classic example of a hash table is DNS (Domain Name System).

DNS is a system that translates Internet domain names into IP addresses.

Imagine you want to visit a website, like [www.google.com](http://www.google.com/).

If DNS were not a hash table, you would have to go through the whole list of domains until you found the site's name. Imagine how long that would take.

But since DNS is a hash table, you can find a site's IP address in constant time, that is, in O(1).

Let's look at an example.

```go
package main

import (
    "fmt"
)
func main() {
    dns := make(map[string]string)
    dns["www.google.com"] = "192.168.5.5"
    dns["www.facebook.com"] = "192.168.40.21"
    dns["www.twitter.com"] = "192.168.11.11"
    fmt.Println("The IP address of www.google.com is", dns["www.google.com"])
    fmt.Println("The IP address of www.facebook.com is", dns["www.facebook.com"])
    fmt.Println("The IP address of www.twitter.com is", dns["www.twitter.com"])
}
```

See how similar these examples are? The idea is the same: the hash table generates an index for each name, and the IP address is returned in constant time.

### 3. Collisions

### 3.1. Introduction

A collision happens when two different lookup keys have the same hash value.

But is that possible? Doesn't the hash function generate a unique value for each key?

Well, a hash function does not generate a unique value for each key; it generates a value that represents the key.

And since there are far more possible keys than hash values, two different keys can end up with the same hash value.

### 3.2. Handling Collisions

There are several ways to handle collisions, but the two most common are:

- Chaining
- Open Addressing

#### 3.2.1. Chaining

With chaining, each entry in the hash table is a linked list.

When a collision happens, the key is added to the corresponding linked list.

This means that if two different keys have the same hash value, they are added to the same linked list.

The main advantage of chaining is that it is simple to implement; the main disadvantage is that it can be inefficient in terms of space and performance.

#### 3.2.2. Open Addressing

With open addressing, when a collision happens, the key is placed in another position of the hash table.

This means that if two different keys have the same hash value, the second key is stored in another position of the table.

The main advantage of open addressing is that it is efficient in terms of space and performance; the main disadvantage is that it is more complex to implement.

### 4. Performance

### 4.1. Load Factor

The load factor is the ratio between the number of keys and the number of positions in the hash table.

The higher the load factor, the higher the chance of collisions.

That is why it is important to keep the load factor low to ensure good hash table performance.

An ideal load factor is below 0.7, meaning less than 70% of the table's positions are occupied.

#### 4.1.1. How to Calculate the Load Factor

The load factor is calculated like this:

```text
NK = number of keys
NP = number of positions in the hash table
```

NK / NP = load factor

For example, if the hash table has 100 positions and 70 keys, the load factor is 0.7.

70 / 100 = 0.7

### 4.2. Resizing

When the load factor goes above 0.7, the hash table needs to be resized.

Resizing works like this:

- Create a new hash table twice the size of the original
- Add all keys from the original table to the new one
- Discard the original table

Resizing a hash table is an expensive operation, but it is necessary to keep its performance good.

### 5. SHA

SHA (Secure Hash Algorithm) is a family of cryptographic hash functions.

It is a great hash function because it produces a practically unique value for each key.

SHA is widely used in cryptography, information security and authentication.

SHA is a very secure hash function, and it is practically impossible to find two different keys with the same hash value.

One of SHA's main advantages is that it is fast, efficient and one-way: it is easy to compute the hash of a key, but practically impossible to recover the key from the hash.

Here is an example of using SHA in Go:

```go
package main
import (
    "crypto/sha256"
    "fmt"
)

func main() {
    word := "hello"
    hash := sha256.Sum256([]byte(word))
    fmt.Printf("The hash of %s is %x\n", word, hash)
}
```

In this example, the SHA-256 hash function is used to generate a hash value for the word “hello”.

But if you try to use SHA-256 to get the word “hello” back from the hash value, you won't be able to.

### 6. Conclusion

Hash tables are very efficient data structures for fast lookups.

They are widely used in real-world applications, such as phone books, DNS, cryptography and information security.

You learned about hash functions, use cases, collisions, performance and SHA.

Never write your own hash function and use it in production; always use the ready-made, optimized hash functions from your programming language.

I hope you learned a lot about hash tables and that you can apply this knowledge in your real-world applications.

If you have any questions or suggestions, leave them in the comments.

See you next time!
