Skip to content

Hash Table: Practice Problems with Solutions

Welcome to the practice problems for hash tables. If you need a refresher on the code, the code templates have the patterns in all 6 languages. Each problem below includes a hint, a visual walkthrough, and the full solution.

Recommended Study Order

Work through the problems in the order they appear. Contains Duplicate teaches the membership check with a set. Two Sum adds the complement search, and Ransom Note adds decrementing counts. The Medium problems each add one twist. Group Anagrams builds a canonical key. Intersection of Two Arrays II combines counting with consumption. Longest Consecutive Sequence turns the set into a linear-time algorithm. LRU Cache is the capstone. It pairs the map with a linked list, and both halves must be correct for it to work.

The order above is designed to build intuition progressively. The app schedules your reviews so you do not forget the Easy patterns while grinding Hard ones. Set up your review schedule .

Easy Problems

1. Contains Duplicate

LeetCode 217 | Difficulty: Easy

Brief: determine whether any value in an array appears more than once.

Why this pattern: membership testing is the purest hash table use. A set records what you have seen, and the first repeat is the answer.

Key Insight: you do not need counts. Existence is enough, which is exactly what a set provides.

Visual:

    graph LR
    A["nums = [1,2,3,1]"] --> B["seen = {}"]
    B --> C["1: not seen, add"]
    C --> D["2: not seen, add"]
    D --> E["3: not seen, add"]
    E --> F["1: already seen, return true"]
  

Code:

var containsDuplicate = function(nums) {
    const seen = new Set();

    for (const num of nums) {
        if (seen.has(num)) {
            return true;
        }
        seen.add(num);
    }

    return false;
};

The early exit matters. As soon as a duplicate appears, the answer is known and the loop stops. The worst case is an array with no duplicates, where every element ends up in the set. Time is

O(N)
and space is
O(N)
.

2. Two Sum

LeetCode 1 | Difficulty: Easy

Brief: return the indices of the two numbers that add up to a target.

Why this pattern: complement search. For each number, the partner you need is the target minus the number. Store what you have seen, check what you still need.

Key Insight: check the map before storing the current number. Otherwise an element can pair with itself.

Visual:

    graph LR
    A["nums = [2,7,11,15], target = 9"] --> B["map = {}"]
    B --> C["2: need 7. Not seen. Store 2 at index 0"]
    C --> D["7: need 2. Seen at index 0. Return [0, 1]"]
  

Code:

var twoSum = function(nums, target) {
    const seen = new Map();

    for (let i = 0; i < nums.length; i++) {
        const complement = target - nums[i];

        // Check before storing so the current element
        // cannot pair with itself.
        if (seen.has(complement)) {
            return [seen.get(complement), i];
        }

        seen.set(nums[i], i);
    }

    return [];
};

The map stores value to index, and each element is visited once. Time is

O(N)
and space is
O(N)
. The check-then-store order is the one detail that separates a bug from a solution, and it is worth tracing with nums = [3] and target = 6 to see why.

3. Ransom Note

LeetCode 383 | Difficulty: Easy

Brief: decide whether the letters in a magazine can build a ransom note, with each letter used at most once.

Why this pattern: frequency counting with decrement. Count the magazine letters, then consume them one by one for the note.

Key Insight: counts, not booleans. A letter used twice in the note needs two copies in the magazine.

Visual:

    graph LR
    A["magazine = aab"] --> B["counts: a=2, b=1"]
    B --> C["note = aba"]
    C --> D["a: 2 to 1. b: 1 to 0. a: 1 to 0"]
    D --> E["All letters found. Return true"]
  

Code:

var canConstruct = function(ransomNote, magazine) {
    const counts = new Map();

    for (const ch of magazine) {
        counts.set(ch, (counts.get(ch) || 0) + 1);
    }

    for (const ch of ransomNote) {
        if (!counts.has(ch) || counts.get(ch) === 0) {
            return false;
        }
        counts.set(ch, counts.get(ch) - 1);
    }

    return true;
};

The default value does the work. A letter that is not in the magazine reads as 0 and fails immediately, and an empty note passes because the second loop never runs. Each letter is handled once, so time is

O(N)
and space is
O(K)
where K is the number of distinct letters.

Medium Problems

4. Group Anagrams

LeetCode 49 | Difficulty: Medium

Brief: group strings that are anagrams of each other.

Why this pattern: grouping by a canonical key. Two anagrams share the same sorted form, so the sorted form is the key and the strings collect under it.

Key Insight: the key must be identical for every anagram and different otherwise. Sorting the letters gives exactly that.

Visual:

    graph LR
    A["eat"] --> B["key = aet"]
    C["tea"] --> B
    D["ate"] --> B
    B --> E["groups[aet] = [eat, tea, ate]"]
    F["tan"] --> G["key = ant"]
    G --> H["groups[ant] = [tan]"]
  

Code:

var groupAnagrams = function(strs) {
    const groups = new Map();

    for (const str of strs) {
        // Sorted letters are the canonical form:
        // every anagram produces the same key.
        const key = str.split('').sort().join('');

        if (!groups.has(key)) {
            groups.set(key, []);
        }
        groups.get(key).push(str);
    }

    return Array.from(groups.values());
};

Each string costs a sort, so the total is

O(N)
times the cost of sorting each string, where N is the number of strings and the sort cost depends on string length. Space is
O(N)
for the map. This problem also appears on the string manipulation problems page , framed around the string transforms themselves.

5. Intersection of Two Arrays II

LeetCode 350 | Difficulty: Medium

Brief: return the elements common to two arrays, including duplicates.

Why this pattern: frequency counting with consumption. Count one array, then take from the count for the other.

Key Insight: each element of the result must consume one unit of count, so a number cannot be counted more times than it appears.

Visual:

    graph LR
    A["nums1 = [1,2,2,1]"] --> B["freq: 1=2, 2=2"]
    B --> C["nums2 = [2,2]"]
    C --> D["2: count 2. Add. Count becomes 1"]
    D --> E["2: count 1. Add. Count becomes 0"]
    E --> F["result = [2,2]"]
  

Code:

var intersect = function(nums1, nums2) {
    const freq = new Map();
    const result = [];

    for (const num of nums1) {
        freq.set(num, (freq.get(num) || 0) + 1);
    }

    for (const num of nums2) {
        if (freq.has(num) && freq.get(num) > 0) {
            result.push(num);
            freq.set(num, freq.get(num) - 1);
        }
    }

    return result;
};

The decrement is the point. Without it, a number appearing twice in nums2 would be counted twice even if nums1 had it once. Time and space are both

O(N)
in the size of the larger array.

6. Longest Consecutive Sequence

LeetCode 128 | Difficulty: Medium

Brief: find the length of the longest run of consecutive numbers. The solution must run in

O(N)
time.

Why this pattern: set membership checks. Store every number in a set, then walk each run forward.

Key Insight: only start counting at the beginning of a run. A number starts a run when its predecessor is not in the set. Every run gets counted exactly once, which is what keeps the time linear.

Visual:

    graph LR
    A["nums = [100,4,200,1,3,2]"] --> B["set = {1,2,3,4,100,200}"]
    B --> C["1: 0 not in set. Start. Run is 1,2,3,4. Length 4"]
    C --> D["100: 99 not in set. Start. Run is 100. Length 1"]
    D --> E["200: 199 not in set. Start. Run is 200. Length 1"]
    E --> F["longest = 4"]
  

Code:

var longestConsecutive = function(nums) {
    const numSet = new Set(nums);
    let longest = 0;

    for (const num of numSet) {
        // Only begin at the start of a run. If num - 1
        // exists, this number belongs to a longer run
        // and will be counted when that run starts.
        if (!numSet.has(num - 1)) {
            let current = num;
            let length = 1;

            while (numSet.has(current + 1)) {
                current++;
                length++;
            }

            longest = Math.max(longest, length);
        }
    }

    return longest;
};

The linear time depends on starting only at run beginnings. Each number is visited at most twice: once when checking whether it is a run start, and once while its own run is being walked. The while loop never re-walks a run because only the first element of a run enters the loop. Space is

O(N)
for the set.

Hard Problems

7. LRU Cache

LeetCode 146 | Difficulty: Hard

Brief: design a cache with

O(1)
get and put that evicts the least recently used entry when full.

Why this pattern: the map alone cannot track order. A doubly linked list provides

O(1)
moves, and the map provides
O(1)
access to any node.

Key Insight: the head of the list is the most recently used entry and the tail is the least recently used. get and put both move a node to the head. When the cache is full, the tail node is removed from both the list and the map.

Visual:

    graph TD
    A["get(key)"] --> B{"key in map?"}
    B -->|No| C["return -1"]
    B -->|Yes| D["move node to head"]
    D --> E["return value"]
    F["put(key, value)"] --> G{"key in map?"}
    G -->|Yes| H["update value, move to head"]
    G -->|No| I{"at capacity?"}
    I -->|Yes| J["remove tail node and its map entry"]
    I -->|No| K["insert new node at head"]
    J --> K
  

Code:

class Node {
    constructor(key, value) {
        this.key = key;
        this.value = value;
        this.prev = null;
        this.next = null;
    }
}

class LRUCache {
    constructor(capacity) {
        this.capacity = capacity;
        this.cache = new Map();
        this.head = new Node(0, 0);
        this.tail = new Node(0, 0);
        // Dummy head and tail keep the list edges
        // from needing special cases on insert and remove.
        this.head.next = this.tail;
        this.tail.prev = this.head;
    }

    get(key) {
        if (!this.cache.has(key)) return -1;

        const node = this.cache.get(key);
        this._remove(node);
        this._addToHead(node);
        return node.value;
    }

    put(key, value) {
        if (this.cache.has(key)) {
            const node = this.cache.get(key);
            node.value = value;
            this._remove(node);
            this._addToHead(node);
        } else {
            if (this.cache.size >= this.capacity) {
                const tail = this._removeTail();
                // The map and the list must agree. Evicting
                // from the list without the map leaks the key.
                this.cache.delete(tail.key);
            }
            const node = new Node(key, value);
            this._addToHead(node);
            this.cache.set(key, node);
        }
    }

    _remove(node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    _addToHead(node) {
        node.prev = this.head;
        node.next = this.head.next;
        this.head.next.prev = node;
        this.head.next = node;
    }

    _removeTail() {
        const node = this.tail.prev;
        this._remove(node);
        return node;
    }
}

The invariant that holds everything together is that the map and the list always describe the same entries. When a node moves, no map change is needed, because the map points at the node rather than at a position. When a node is evicted, its map entry goes with it. get and put are each

O(1)
: one map lookup and a constant number of pointer updates.

Hard problems are where patterns separate candidates who pass from candidates who get offers. Track your misses and review them on a spaced schedule so you do not repeat them in the real interview.

These seven problems cover the full range of hash table work. Start with the membership check in Contains Duplicate, add the complement search in Two Sum, and build up to the combined structure in LRU Cache. By the end, you should recognize when a problem is asking for a set and when it needs a full map.

Done with these problems? The app has more, plus a spaced repetition system that brings problems back right before you would forget them. Continue your prep .