706. Design HashMap
Problem Description
This problem asks you to implement a basic HashMap data structure from scratch without using any built-in hash table libraries.
A HashMap is a data structure that stores key-value pairs and provides fast access to values based on their keys. You need to create a class called MyHashMap
with the following functionality:
-
Constructor (
MyHashMap()
): Creates an empty HashMap when initialized. -
Put operation (
put(key, value)
): Stores a key-value pair in the HashMap. If the key already exists, the method should update its value with the new one provided. -
Get operation (
get(key)
): Retrieves the value associated with a given key. If the key doesn't exist in the HashMap, it should return-1
. -
Remove operation (
remove(key)
): Deletes a key and its associated value from the HashMap if the key exists.
The solution shown uses a direct addressing approach with an array of size 1,000,001. This approach works because the problem constraints guarantee that keys will be in the range [0, 10^6]
. Each index in the array represents a possible key, and the value at that index represents the corresponding value in the HashMap. The implementation initializes all positions to -1
to indicate empty slots. When put
is called, it directly assigns the value to data[key]
. The get
operation simply returns data[key]
, and remove
sets data[key]
back to -1
.
While this solution is simple and provides O(1) time complexity for all operations, it uses O(10^6) space regardless of how many elements are actually stored, making it space-inefficient for sparse data.
Intuition
The key insight for solving this problem is recognizing that we need a way to map keys to values with fast access times. Since we're not allowed to use built-in hash tables, we need to think about the fundamental principle behind how hash maps work.
At its core, a hash map uses an array where we can directly access any position using an index. The challenge is usually converting arbitrary keys into array indices. However, when we look at the problem constraints, we notice that keys are limited to the range [0, 10^6]
. This is a crucial observation.
Since the keys are already integers within a fixed range, we can use them directly as array indices without any hashing function. This eliminates the complexity of dealing with hash collisions and designing hash functions. We can simply create an array large enough to accommodate all possible keys.
The thought process goes like this:
- We need O(1) access time for put, get, and remove operations
- Arrays provide O(1) access time when we know the index
- Our keys are integers from 0 to 1,000,000
- We can use the key itself as the array index
For handling the "empty" state (when a key doesn't exist), we need a sentinel value. Since the problem specifies returning -1
for non-existent keys, we can initialize our entire array with -1
. This way, any unset position naturally returns the correct value for get
operations.
This direct addressing approach trades space for simplicity and speed. While it uses more memory than necessary (especially when storing few elements), it completely avoids the complexities of collision resolution strategies like chaining or open addressing that would be needed in a more space-efficient implementation.
Learn more about Linked List patterns.
Solution Approach
The implementation uses a direct addressing table approach with a fixed-size array. Here's how each component works:
Data Structure:
- We use a single array
self.data
of size 1,000,001 to store all key-value mappings - Each index in the array represents a possible key (0 to 1,000,000)
- The value at each index represents the corresponding value for that key
Initialization (__init__
):
self.data = [-1] * 1000001
- Creates an array with 1,000,001 elements, all initialized to
-1
- The value
-1
serves as a marker for "no value exists at this key" - This initialization ensures that
get
operations on non-existent keys automatically return-1
Put Operation (put(key, value)
):
self.data[key] = value
- Directly assigns the value to the array at index
key
- If a value already exists at that key, it gets overwritten (update behavior)
- Time complexity: O(1) - direct array access
Get Operation (get(key)
):
return self.data[key]
- Simply returns the value stored at index
key
- If the key was never set, it returns
-1
(the initialization value) - Time complexity: O(1) - direct array access
Remove Operation (remove(key)
):
self.data[key] = -1
- Sets the value at index
key
back to-1
- This effectively marks the key as "removed" or "non-existent"
- Time complexity: O(1) - direct array access
Trade-offs of this approach:
- Advantages: Extremely simple implementation, guaranteed O(1) time for all operations, no collision handling needed
- Disadvantages: Uses O(10^6) space regardless of the number of elements stored, only works when keys are integers in a known range
This solution leverages the constraint that keys are bounded integers to eliminate the need for a hash function entirely, making it a perfect example of how problem constraints can lead to simplified solutions.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a sequence of operations to see how our HashMap works:
Initial State:
data = [-1, -1, -1, -1, -1, ...] (1,000,001 elements, all -1)
Operation 1: put(2, 10)
- We want to store value 10 with key 2
- We directly access
data[2]
and set it to 10
data = [-1, -1, 10, -1, -1, ...] ^ ^ ^ key=0 key=1 key=2
Operation 2: put(5, 25)
- Store value 25 with key 5
- Set
data[5] = 25
data = [-1, -1, 10, -1, -1, 25, -1, ...] ^ key=5
Operation 3: get(2)
- Retrieve the value for key 2
- Simply return
data[2]
, which is 10 - Returns: 10
Operation 4: get(3)
- Retrieve the value for key 3
- Return
data[3]
, which is -1 (never set) - Returns: -1 (indicating key doesn't exist)
Operation 5: put(2, 15)
- Update the value for key 2 from 10 to 15
- Set
data[2] = 15
(overwrites the old value)
data = [-1, -1, 15, -1, -1, 25, -1, ...] ^ key=2 (updated)
Operation 6: remove(5)
- Remove the key-value pair for key 5
- Set
data[5] = -1
(back to "empty" state)
data = [-1, -1, 15, -1, -1, -1, -1, ...] ^ key=5 (removed)
Operation 7: get(5)
- Try to retrieve the value for key 5 (which we just removed)
- Return
data[5]
, which is now -1 - Returns: -1 (indicating key doesn't exist anymore)
This example demonstrates how:
- The key directly maps to the array index (no hashing needed)
- Put operations simply store values at the corresponding index
- Get operations return whatever is at that index (-1 if never set or removed)
- Remove operations reset the position to -1
- All operations take O(1) time since they're just direct array accesses
Solution Implementation
1class MyHashMap:
2 def __init__(self):
3 """
4 Initialize the HashMap data structure.
5 Using direct addressing with an array to store key-value pairs.
6 Array size is 10^6 + 1 to handle keys in range [0, 10^6].
7 """
8 # Initialize array with -1 to indicate empty slots
9 self.data = [-1] * 1000001
10
11 def put(self, key: int, value: int) -> None:
12 """
13 Insert or update a key-value pair in the HashMap.
14
15 Args:
16 key: The key to insert/update (0 <= key <= 10^6)
17 value: The value to associate with the key
18 """
19 # Direct addressing: use key as index
20 self.data[key] = value
21
22 def get(self, key: int) -> int:
23 """
24 Retrieve the value associated with the given key.
25
26 Args:
27 key: The key to look up
28
29 Returns:
30 The value associated with the key, or -1 if key doesn't exist
31 """
32 # Return value at index 'key' (-1 if not present)
33 return self.data[key]
34
35 def remove(self, key: int) -> None:
36 """
37 Remove the key-value pair from the HashMap.
38
39 Args:
40 key: The key to remove
41 """
42 # Reset to -1 to indicate the key is removed
43 self.data[key] = -1
44
45
46# Your MyHashMap object will be instantiated and called as such:
47# obj = MyHashMap()
48# obj.put(key, value)
49# param_2 = obj.get(key)
50# obj.remove(key)
51
1/**
2 * Implementation of a simple hash map using direct addressing with an array.
3 * This approach uses the key directly as an index in the array.
4 * Constraint: Keys are in the range [0, 1000000]
5 */
6class MyHashMap {
7 // Array to store key-value pairs, where index represents the key
8 // -1 indicates that no value exists for that key
9 private int[] data;
10
11 // Maximum possible key value plus one
12 private static final int MAX_KEY_RANGE = 1000001;
13
14 // Sentinel value to indicate absence of a key-value pair
15 private static final int EMPTY_VALUE = -1;
16
17 /**
18 * Constructor initializes the hash map with all positions set to EMPTY_VALUE
19 */
20 public MyHashMap() {
21 data = new int[MAX_KEY_RANGE];
22 Arrays.fill(data, EMPTY_VALUE);
23 }
24
25 /**
26 * Inserts or updates a key-value pair in the hash map
27 * @param key the key to insert or update (0 <= key <= 1000000)
28 * @param value the value to associate with the key
29 */
30 public void put(int key, int value) {
31 data[key] = value;
32 }
33
34 /**
35 * Retrieves the value associated with the given key
36 * @param key the key to look up
37 * @return the value associated with the key, or -1 if key doesn't exist
38 */
39 public int get(int key) {
40 return data[key];
41 }
42
43 /**
44 * Removes the key-value pair from the hash map
45 * @param key the key to remove
46 */
47 public void remove(int key) {
48 data[key] = EMPTY_VALUE;
49 }
50}
51
52/**
53 * Your MyHashMap object will be instantiated and called as such:
54 * MyHashMap obj = new MyHashMap();
55 * obj.put(key,value);
56 * int param_2 = obj.get(key);
57 * obj.remove(key);
58 */
59
1class MyHashMap {
2private:
3 // Array to store key-value pairs, using index as key
4 // Size 1000001 to accommodate keys from 0 to 1000000
5 int data[1000001];
6
7public:
8 // Constructor: Initialize all elements to -1 (indicating empty/not present)
9 MyHashMap() {
10 memset(data, -1, sizeof(data));
11 }
12
13 // Insert or update a key-value pair in the hash map
14 void put(int key, int value) {
15 data[key] = value;
16 }
17
18 // Retrieve the value associated with the given key
19 // Returns -1 if the key doesn't exist
20 int get(int key) {
21 return data[key];
22 }
23
24 // Remove the key-value pair from the hash map
25 // Sets the value to -1 to indicate the key is no longer present
26 void remove(int key) {
27 data[key] = -1;
28 }
29};
30
31/**
32 * Your MyHashMap object will be instantiated and called as such:
33 * MyHashMap* obj = new MyHashMap();
34 * obj->put(key, value);
35 * int param_2 = obj->get(key);
36 * obj->remove(key);
37 */
38
1// Array to store key-value pairs, using index as key and element value as the stored value
2// Initialize with -1 to indicate empty slots (assuming -1 is not a valid value)
3let hashMapData: number[] = new Array(10 ** 6 + 1).fill(-1);
4
5/**
6 * Inserts or updates a key-value pair in the hash map
7 * @param key - The key to insert/update (must be within array bounds)
8 * @param value - The value to associate with the key
9 */
10function put(key: number, value: number): void {
11 hashMapData[key] = value;
12}
13
14/**
15 * Retrieves the value associated with the given key
16 * @param key - The key to look up
17 * @returns The value associated with the key, or -1 if key doesn't exist
18 */
19function get(key: number): number {
20 return hashMapData[key];
21}
22
23/**
24 * Removes the key-value pair from the hash map
25 * @param key - The key to remove
26 */
27function remove(key: number): void {
28 hashMapData[key] = -1;
29}
30
Time and Space Complexity
Time Complexity:
__init__()
:O(1000001)
orO(10^6)
- initializes an array of size 1000001 with -1 valuesput(key, value)
:O(1)
- direct array access and assignment at indexkey
get(key)
:O(1)
- direct array access at indexkey
remove(key)
:O(1)
- direct array access and assignment at indexkey
Space Complexity:
O(1000001)
orO(10^6)
- the implementation uses a fixed-size array of 1000001 elements regardless of how many key-value pairs are actually stored. This is constant space with respect to the constraint that keys are in the range[0, 10^6]
, but represents significant memory overhead when few keys are used.
Analysis:
This implementation uses a direct-address table approach where each possible key (0 to 1000000) maps directly to an array index. While this provides optimal O(1)
time complexity for all operations, it trades off space efficiency by allocating memory for all possible keys upfront, even if only a small subset of keys are actually used.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Memory Inefficiency for Sparse Data
The most significant pitfall of this direct addressing approach is the massive memory waste when dealing with sparse data. Even if you only store a single key-value pair, the solution allocates memory for 1,000,001 integers.
Example scenario:
hashmap = MyHashMap() hashmap.put(999999, 100) # Only one element, but using ~4MB of memory
Solution: Implement a proper hash table with collision handling:
class MyHashMap:
def __init__(self):
self.size = 1000 # Much smaller initial size
self.buckets = [[] for _ in range(self.size)]
def _hash(self, key):
return key % self.size
def put(self, key: int, value: int) -> None:
hash_key = self._hash(key)
bucket = self.buckets[hash_key]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
bucket.append((key, value))
def get(self, key: int) -> int:
hash_key = self._hash(key)
bucket = self.buckets[hash_key]
for k, v in bucket:
if k == key:
return v
return -1
def remove(self, key: int) -> None:
hash_key = self._hash(key)
bucket = self.buckets[hash_key]
for i, (k, v) in enumerate(bucket):
if k == key:
del bucket[i]
return
2. Inflexibility with Key Constraints
The current implementation breaks if keys fall outside the [0, 10^6] range, causing index out of bounds errors.
Problem example:
hashmap = MyHashMap() hashmap.put(-1, 50) # IndexError: list index out of range hashmap.put(1000001, 60) # IndexError: list index out of range
Solution: Add boundary checking or use a dynamic approach:
def put(self, key: int, value: int) -> None:
if 0 <= key <= 1000000:
self.data[key] = value
# Or raise an exception for invalid keys
3. Confusion with Special Value -1
Using -1
as both a sentinel value for "empty" and a potential legitimate value can cause ambiguity. If the problem allowed storing -1
as a valid value, this approach would fail.
Problematic scenario:
hashmap.put(5, -1) # Storing -1 as a legitimate value hashmap.remove(5) # Also sets to -1 # Now we can't distinguish between "key 5 has value -1" vs "key 5 doesn't exist"
Solution: Use a separate data structure to track existence:
class MyHashMap:
def __init__(self):
self.data = [0] * 1000001
self.exists = [False] * 1000001
def put(self, key: int, value: int) -> None:
self.data[key] = value
self.exists[key] = True
def get(self, key: int) -> int:
if self.exists[key]:
return self.data[key]
return -1
def remove(self, key: int) -> None:
self.exists[key] = False
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Linked List Cycle Given a linked list with potentially a loop determine whether the linked list from the first node contains a cycle in it For bonus points do this with constant space Parameters nodes The first node of a linked list with potentially a loop Result Whether there is a loop contained
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!