V Talks Tech · 13 MAR 2026

Data Structures #2: The Hash Table - The Magic of O(1)

Explore how hash tables use hashing and collision resolution for O(1) performance, enabling efficient data retrieval with non-numeric keys across all major programming languages.

By
Vibhishan Ranga
Read
6 min
Tags
data structureshashsethashmap
Cover image for Data Structures #2: The Hash Table - The Magic of O(1)

In our last post, we established the power of the array: its ability to access any element in constant O(1) time using an index. But this superpower comes with a major limitation. What if you don't have an index? What if you want to find a user's data by their username (a string), not by a number? In an array, you'd have no choice but to search from the beginning, an O(n) operation.

This is the problem the Hash Table was born to solve. It's a work of genius that allows us to store and retrieve data using non-numeric keys, like strings, while achieving that magical average-case O(1) performance.

At its core, a hash table is a clever combination of two things we already know: an array and a special function.

Under the Hood: The Hashing Function, Buckets, and Collisions

To understand a hash table, you need to understand three core concepts:

  1. The Underlying Array: Deep down, a hash table is just an array. We often call the slots in this array "buckets."

  2. The Hashing Function: This is the secret sauce. A hashing function is a procedure that takes a key of any type (like a string, an object, etc.) and converts it into an integer, which we can then use as an index for our underlying array.

  3. Collision Resolution: Here's the catch. What happens if our hashing function generates the same index for two different keys? This is called a collision, and how a hash table handles it is fundamental to its design.

Let's imagine we want to store the ages of users. Our hash function might take their username (a string) and produce an array index.

hash("john_smith") -> 2 hash("lisa_jones") -> 4 hash("sam_doe") -> 2 // That is a collision!

Both "john_smith" and "sam_doe" map to index 2. The most common way to resolve this is called Separate Chaining. Instead of storing the value directly in the array, each bucket (array slot) holds a pointer to a linked list of all the key-value pairs that hashed to that index.

When we look up "sam_doe", we first hash it to get index 2. We then go to bucket 2 and traverse the short linked list until we find the node with the key "sam_doe". As long as the hash function distributes keys evenly and the lists remain short, this entire process is incredibly fast, close to O(1).

Hash Sets vs. Hash Maps

Hash tables come in two main flavors, distinguished by what they store:

  • Hash Map (or Dictionary): Stores key-value pairs. This is the most common form. Think of Python's dict or a JavaScript Map. You use a key to look up an associated value (e.g., username -> user profile object).

  • Hash Set: Stores only unique keys. The main goal is to efficiently check for the presence or absence of an item. contains("john_smith") would return true. You can think of a set as a map where the value is always just a placeholder.

The Dynamic Dilemma: Load Factor and Rehashing

Just like dynamic arrays, hash tables need to grow. If we have 8 elements and only 10 buckets, collisions will become very frequent, and the linked lists will get long, degrading performance back towards O(n).

To manage this, we track the Load Factor:

Load Factor = (Number of Elements) / (Number of Buckets)

Most languages have a threshold for this factor, typically around 0.75. Once the load factor exceeds this, the hash table performs a resizing operation:

  1. A new, larger array is allocated in memory (usually 2x the original size).

  2. Crucially, you must rehash! You cannot just copy the old buckets. You must iterate through every single key-value pair in the old table, recalculate its hash based on the new, larger size, and insert it into the correct bucket in the new table.

This rehashing is an O(n) operation, but like with dynamic arrays, its cost is amortized over many insertions, so the average time for an add operation remains O(1).

Language Implementations: The Universal Data Structure

Hash tables are so useful they are built into the core of most modern languages.

C++: std::unordered_map & std::unordered_set

C++ provides explicit hash table implementations in its standard library.

#include <iostream>
#include <string>
#include <unordered_map>
#include <unordered_set>

int main() {
    // Hash Map: stores key-value pairs
    std::unordered_map<std::string, int> user_ages;
    user_ages["alice"] = 30;
    user_ages["bob"] = 25;

    std::cout << "Alice's age: " << user_ages["alice"] << std::endl;

    // Hash Set: stores unique keys
    std::unordered_set<std::string> unique_users;
    unique_users.insert("alice");
    if (unique_users.count("alice")) {
        std::cout << "Alice is a unique user." << std::endl;
    }
    return 0;
}
  • Internals: The C++ standard requires these to have average O(1) complexity and worst-case O(n). They are typically implemented using separate chaining with linked lists.

Java: HashMap & HashSet

Java's implementations are robust and highly optimized.

import java.util.HashMap;
import java.util.HashSet;

class Main {
    public static void main(String[] args) {
        // HashMap
        HashMap<String, Integer> userAges = new HashMap<>();
        userAges.put("alice", 30);
        userAges.put("bob", 25);
        System.out.println("Bob's age: " + userAges.get("bob"));

        // HashSet
        HashSet<String> uniqueUsers = new HashSet<>();
        uniqueUsers.add("alice");
        if (uniqueUsers.contains("alice")) {
            System.out.println("Alice exists.");
        }
    }
}
  • Internals: Java's HashMap has a default initial capacity of 16 and a load factor of 0.75. In a clever optimization since Java 8, if a bucket's linked list becomes too long (8+ items), it is converted into a self-balancing binary search tree, improving the worst-case lookup from O(n) to O(log n).

Python: dict & set

The dictionary (dict) is arguably the most important data structure in Python and is fundamental to how the language itself works.

# The dict is Python's hash map
user_ages = {
    "alice": 30,
    "bob": 25
}
print(f"Bob's age: {user_ages['bob']}")

# The set is Python's hash set
unique_users = {"alice", "bob"}
if "alice" in unique_users:
    print("Alice exists.")
  • Internals: Python's dictionaries are highly optimized. Since Python 3.7, they also remember the insertion order of keys, a feature that was not guaranteed in older versions.

JavaScript: Map & Set

While plain Objects can be used as hash maps, the modern Map and Set objects are preferred as they are designed specifically for this purpose and avoid certain legacy quirks.

// Map
const userAges = new Map();
userAges.set('alice', 30);
userAges.set('bob', 25);
console.log(`Bob's age: ${userAges.get('bob')}`);

// Set
const uniqueUsers = new Set();
uniqueUsers.add('alice');
if (uniqueUsers.has('alice')) {
  console.log('Alice exists.');
}
  • Internals: Modern JavaScript engines optimize maps heavily, providing performance that is, on average, O(1).

Famous Algorithms & Use Cases

Hash tables are not just for storage; they are problem-solving machines.

Two Sum Problem

A classic interview question: "Given an array of numbers and a target, find two numbers that add up to it." A hash map solves this in a single O(n) pass. As you iterate, for each number x, you check if target - x is already in your map. If it is, you've found a pair. If not, you add x to the map and continue.

Checking for Duplicates

The simplest and most common use case for a hash set. To find if an array has duplicates, iterate through it, adding each element to a set. If you ever try to add an element that's already in the set, you've found a duplicate. This is an O(n) solution.

Caching / Memoization

Hash maps are the foundation of caching. When you have an expensive function call, you can store its result in a map with the function's arguments as the key. The next time the function is called with the same arguments, you can return the cached result in O(1) time instead of re-computing it.

Conclusion: The Index for Everything

If the array is the fundamental building block of memory, the hash table is the fundamental building block of data association. It provides a bridge between the human world of meaningful keys and the computer's world of array indices.

From the symbol tables in a compiler, to the routing tables that direct internet traffic, to the simple act of converting a JSON string into a usable object in memory, the hash table is the unsung hero that makes it all happen, and makes it happen fast.

All posts →

Pit wall · Newsletter

Want to stay updated?

Get new posts in your inbox as soon as they're out. No spam, just the latest from V Talks Tech.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.