Hash Map: Complete Implementation Guide
1. Introduction and Core Concepts
A hash map, also known as a hash table, dictionary, or associative array, is one of the most fundamental and widely used data structures in computer science. At its core, a hash map provides a way to store key-value pairs and retrieve values by their associated keys with remarkable efficiency. Unlike arrays that require integer indices or linked lists that require linear traversal, a hash map allows you to use virtually any immutable data type as a key — strings, integers, tuples, or custom objects — and locate the corresponding value in constant time on average.
The importance of the hash map cannot be overstated. It is the backbone of countless software systems, from programming language runtimes to database indexing engines. In Python, the dict type is a highly optimized hash map. In Java, it appears as HashMap, in JavaScript as Map and plain objects, and in C++ as unordered_map. Understanding how a hash map works internally is essential not just for passing technical interviews, but for making informed architectural decisions when performance, memory, and correctness matter.
Conceptually, a hash map solves the problem of fast lookup. Imagine you need to store user profiles indexed by username, cache web responses by URL, or count word frequencies in a document. A naive approach using a list would require scanning every element to find a match, resulting in O(n) time. A hash map transforms the key into an array index using a hash function, allowing direct access to the bucket where the value is stored. This transformation is what enables average-case O(1) operations for insertion, deletion, and lookup.
Key Terminology
To understand hash maps deeply, you must be familiar with several core terms. The hash function is a deterministic function that converts a key into an integer hash code. The bucket or slot is the storage location in the underlying array where entries are placed. The load factor is the ratio of stored entries to total buckets (n/m), which determines when the table should resize. A collision occurs when two different keys hash to the same bucket index, a situation that must be handled gracefully for correctness.
A well-designed hash map balances speed, memory usage, and collision resistance. The quality of the hash function, the strategy for resolving collisions, and the policy for resizing all directly impact performance. A poor hash function that clusters keys together can degrade performance to O(n), while a good one distributes keys uniformly across the available buckets, preserving constant-time behavior even under heavy load.
2. How It Works
The operation of a hash map can be broken down into three fundamental steps: hashing, indexing, and collision resolution. When you insert a key-value pair like ("apple", 5), the hash map first applies its hash function to the key. In Python, this is hash("apple"), which returns a large integer, often 64-bit. This integer is then compressed to fit the current table size using the modulo operation: index = hash(key) % capacity. If the capacity is 16, the hash code is mapped to an index between 0 and 15, determining which bucket will store the entry.
Ideal hash functions exhibit three critical properties. First, they must be deterministic: the same key must always produce the same hash code. Second, they should be uniform, distributing keys evenly across the hash space to minimize collisions. Third, they should be efficient to compute, since hashing occurs on every operation. For strings, common algorithms like MurmurHash, FNV-1a, and DJB2 iterate over characters and mix bits to achieve good distribution. For integers, a simple modulo or bit-mixing function often suffices, though cryptographic hashes like SHA-256 are never used due to their computational cost.
Collisions are inevitable, even with excellent hash functions, due to the pigeonhole principle. If you have more possible keys than buckets, some keys must share a bucket. There are two primary families of collision resolution. Separate chaining stores a secondary data structure, typically a linked list or dynamic array, in each bucket. All keys that hash to the same index are appended to that bucket's chain. Open addressing stores all entries directly in the bucket array itself. When a collision occurs, it probes for the next available slot using a deterministic sequence such as linear probing, quadratic probing, or double hashing.
Load Factor and Rehashing
As more entries are added, buckets fill up and collisions become more frequent, increasing lookup time. To maintain performance, hash maps monitor their load factor. When the load factor exceeds a threshold, typically 0.7 to 0.75, the map triggers a resize operation. It allocates a new, larger array, usually double the previous capacity, and rehashes every existing entry into the new array. This rehashing is an expensive O(n) operation, but it happens infrequently enough that the amortized cost of insertion remains O(1). Choosing the right initial capacity and load factor threshold allows you to trade memory for speed.
Deletion introduces additional complexity, especially for open addressing. Simply removing an entry and marking its slot as empty can break the probe sequence for other keys that collided and were placed further along. To solve this, open-addressed tables use a special tombstone marker to indicate a deleted slot. Searches continue past tombstones, while insertions can reuse them. Chained hash maps handle deletion more simply by removing the node from the bucket's linked list.
3. Implementation
Implementing a hash map from scratch reveals the engineering trade-offs hidden behind high-level dictionary APIs. Below is a complete, production-inspired implementation using separate chaining, which is intuitive, robust, and widely used in educational contexts and many real implementations.
class HashMapChaining:
"""Hash Map with separate chaining and dynamic resizing."""
def __init__(self, capacity=8, load_factor=0.75):
self.capacity = capacity
self.size = 0
self.load_factor = load_factor
self.buckets = [[] for _ in range(self.capacity)]
def _hash(self, key):
# Python's built-in hash, then compress to table size
return hash(key) % self.capacity
def put(self, key, value):
"""Insert or update key with value."""
index = self._hash(key)
bucket = self.buckets[index]
# Update if key already exists
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
# Otherwise append new entry
bucket.append((key, value))
self.size += 1
# Check load factor and resize if needed
if self.size > self.capacity * self.load_factor:
self._resize()
def get(self, key):
"""Retrieve value by key, or None if not found."""
index = self._hash(key)
bucket = self.buckets[index]
for k, v in bucket:
if k == key:
return v
return None
def remove(self, key):
"""Remove key and return True if found."""
index = self._hash(key)
bucket = self.buckets[index]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket.pop(i)
self.size -= 1
return True
return False
def _resize(self):
"""Double capacity and rehash all entries."""
old_buckets = self.buckets
self.capacity *= 2
self.size = 0
self.buckets = [[] for _ in range(self.capacity)]
for bucket in old_buckets:
for k, v in bucket:
self.put(k, v)
This implementation handles the core operations cleanly. The _hash method centralizes index computation, put handles both insertion and updates, and _resize ensures the table never becomes too crowded. Note how we iterate through the bucket list to check for existing keys — this is where collisions are resolved. If many keys collide, that bucket's list grows, and operations on that bucket degrade toward O(n).
Resizing deserves special attention. The implementation above doubles capacity, which is standard because it keeps the amortized cost low and works well with modulo hashing. An alternative is to grow to the next prime number, which can improve distribution for certain hash functions. The rehashing loop calls put for each old entry, which recomputes the index under the new capacity. This is correct but requires resetting self.size to avoid double-counting.
def custom_hash_djb2(key: str) -> int:
"""DJB2 string hash function - simple and well-distributed."""
hash_value = 5381
for char in key:
# hash * 33 + ord(char)
hash_value = ((hash_value << 5) + hash_value) + ord(char)
# Keep within 64-bit to simulate overflow
hash_value &= 0xFFFFFFFFFFFFFFFF
return hash_value
class HashMapCustomHash(HashMapChaining):
def _hash(self, key):
# Use custom hash for strings, fallback to built-in
if isinstance(key, str):
return custom_hash_djb2(key) % self.capacity
return hash(key) % self.capacity
def contains(self, key) -> bool:
return self.get(key) is not None
def keys(self):
result = []
for bucket in self.buckets:
for k, v in bucket:
result.append(k)
return result
def __len__(self):
return self.size
Using a custom hash function like DJB2 gives you control over distribution and helps you understand how non-cryptographic hashes work. The bit-shift << 5 is equivalent to multiplying by 32, so (hash << 5) + hash equals hash * 33, a multiplier empirically shown to produce good avalanche effects for string keys.
class HashMapOpenAddressing:
"""Hash Map with linear probing and tombstones."""
def __init__(self, capacity=8):
self.capacity = capacity
self.size = 0
self.keys_arr = [None] * capacity
self.values_arr = [None] * capacity
self.DELETED = object() # Unique tombstone sentinel
def _hash(self, key):
return hash(key) % self.capacity
def _probe(self, key):
"""Find slot for key: either existing key or first available."""
index = self._hash(key)
first_deleted = -1
for i in range(self.capacity):
probe_index = (index + i) % self.capacity
curr_key = self.keys_arr[probe_index]
if curr_key is None:
# Empty slot - use previously found deleted slot if any
return first_deleted if first_deleted != -1 else probe_index
elif curr_key is self.DELETED:
if first_deleted == -1:
first_deleted = probe_index
elif curr_key == key:
return probe_index
return first_deleted if first_deleted != -1 else -1
def put(self, key, value):
if self.size >= self.capacity * 0.7:
self._resize()
slot = self._probe(key)
if slot == -1:
raise Exception("HashMap is full")
is_new = self.keys_arr[slot] is None or self.keys_arr[slot] is self.DELETED
self.keys_arr[slot] = key
self.values_arr[slot] = value
if is_new:
self.size += 1
def get(self, key):
slot = self._probe(key)
if slot != -1 and self.keys_arr[slot] == key:
return self.values_arr[slot]
return None
def _resize(self):
old_keys = self.keys_arr
old_vals = self.values_arr
self.capacity *= 2
self.keys_arr = [None] * self.capacity
self.values_arr = [None] * self.capacity
self.size = 0
for k, v in zip(old_keys, old_vals):
if k is not None and k is not self.DELETED:
self.put(k, v)
The open addressing implementation shows why tombstones are necessary. The _probe method implements linear probing, scanning forward until it finds the key or an empty slot. It remembers the first tombstone encountered so that insertions can reuse deleted slots without breaking search chains. Without tombstones, deleting a key in the middle of a probe sequence would cause subsequent keys in that sequence to become unreachable.
# Demonstration: word frequency counter using custom HashMap
def count_word_frequencies(words):
freq = HashMapChaining(capacity=16)
for word in words:
current = freq.get(word)
if current is None:
freq.put(word, 1)
else:
freq.put(word, current + 1)
return freq
# Example usage
words = ["apple", "banana", "apple", "orange", "banana", "apple"]
counter = count_word_frequencies(words)
print(counter.get("apple")) # Expected: 3
print(counter.get("banana")) # Expected: 2
print(counter.keys()) # Expected: ['apple', 'banana', 'orange'] (order may vary)
# Handling collisions explicitly
hm = HashMapChaining(capacity=4)
# These keys may collide with small capacity
hm.put("key1", 100)
hm.put("key2", 200)
hm.put("key5", 500) # Likely collides with key1 when capacity=4
print(hm.get("key1")) # Still correctly retrieves 100 via chaining
print(len(hm)) # 3
hm.remove("key2")
print(hm.get("key2")) # None
print(hm.contains("key5")) # True
This final example ties everything together into a practical use case. Counting word frequencies is a classic hash map application that would be O(n²) with a list but becomes O(n) average time with a hash map. The example also verifies that collisions do not cause data loss and that deletion works correctly, which are common points of failure in naive implementations.
4. Step-by-Step Example
To make the abstract concrete, let us walk through a hash map with separate chaining, capacity 5, and a simple hash function hash(key) = sum(ord(c) for c in key) % 5. We will insert three keys: "cat", "act", and "dog". First, compute hash codes. For "cat": 99 + 97 + 116 = 312, 312 % 5 = 2. For "act": 97 + 99 + 116 = 312, also 312 % 5 = 2. For "dog": 100 + 111 + 103 = 314, 314 % 5 = 4. Notice that "cat" and "act" are anagrams and collide at index 2.
Initially, all five buckets are empty: [ [], [], [], [], [] ]. Inserting "cat" → 10: index 2 is empty, so bucket 2 becomes [("cat", 10)]. State: [ [], [], [("cat",10)], [], [] ]. Next, inserting "act" → 20: index 2 already contains "cat", but the key "act" is different, so we append to the chain. Bucket 2 becomes [("cat",10), ("act",20)]. State: [ [], [], [("cat",10), ("act",20)], [], [] ]. Finally, "dog" → 7 hashes to index 4, which is empty, so bucket 4 becomes [("dog",7)]. Final state: [ [], [], [("cat",10), ("act",20)], [], [("dog",7)] ].
Now consider lookup. To get "act", we compute hash("act") % 5 = 2, go to bucket 2, and linearly scan the chain. First entry "cat" does not match, second entry "act" matches, so we return 20. This required two comparisons. To get "dog", we compute index 4 and find it immediately. To get "bird", which was never inserted: 98+105+114+100=417 %5=2, we scan bucket 2, find no match, and return None. The cost of lookup is proportional to the length of the chain at that bucket.
If we continue inserting until the load factor exceeds 0.75, which for capacity 5 is 5 * 0.75 = 3.75, the fourth insertion will trigger a resize. Suppose we insert "bird" → 3 (index 2 again). After insertion, size is 4, load factor is 0.8, so we double capacity to 10 and rehash. New indices: "cat" 312 %10=2, "act" 312%10=2, "dog" 314%10=4, "bird" 417%10=7. The new table becomes [ [], [], [("cat",10),("act",20)], [], [("dog",7)], [], [], [("bird",3)], [], [] ]. Notice the distribution improves with larger capacity, though "cat" and "act" still collide because they have identical hash codes.
5. Complexity Analysis
The performance of a hash map is typically described as O(1) average time, but this hides important nuances about best, worst, and amortized cases. The average-case constant time holds only under the assumption of a good hash function that distributes keys uniformly and a load factor kept below a threshold. When these assumptions break, performance can degrade significantly.
Insertion, lookup, and deletion all share the same complexity profile because they all begin with hashing and then handle collisions. In the best case, there are no collisions, the target bucket is empty or contains exactly the desired key, and the operation completes in a single array access. In the average case, with a load factor α = n/m, the expected chain length for chaining is α, so operations take O(1 + α) time. Since α is bounded by a constant (e.g., 0.75), this simplifies to O(1). In the worst case, where all keys collide into a single bucket, operations degenerate to scanning a linked list of length n, giving O(n) time.
Resizing is the most expensive operation, requiring O(n) time to rehash all entries. However, because resizing doubles capacity, it occurs exponentially less frequently as the table grows. The total cost of n insertions with resizing is O(n), so the amortized cost per insertion remains O(1). This is why hash maps are considered to have amortized constant-time insertion, even though a single insertion that triggers resize is linear.
| Operation | Best Case | Average Case | Worst Case | Space Complexity |
|---|---|---|---|---|
| Insert (put) | O(1) | O(1) amortized | O(n) | O(n + m) |
| Search (get) | O(1) | O(1) | O(n) | O(n + m) |
| Delete (remove) | O(1) | O(1) | O(n) | O(n + m) |
| Resize / Rehash | O(n) | O(n) | O(n) | O(n + m) → O(2m) |
| Iterate All Keys | O(n) | O(n + m) | O(n + m) | O(1) extra |
Space complexity is O(n + m), where n is the number of entries and m is the number of buckets. For chaining, each bucket stores a list overhead plus nodes for entries. For open addressing, space is simply O(m), but m must be larger than n to keep the load factor low, typically m ≈ 1.33n to 2n. In practice, hash maps trade memory for speed: a lower load factor uses more empty buckets but reduces collisions, while a higher load factor saves memory at the cost of longer chains or probe sequences.
6. Variations and Optimizations
Beyond basic chaining and linear probing, several sophisticated variations address specific weaknesses. Robin Hood hashing is an optimization for open addressing that reduces variance in probe lengths. When inserting, if the new key has probed further than the key currently occupying a slot, they swap, so the key that has traveled further gets to stay closer to its ideal position. This evens out lookup times and improves tail latency, which is critical for real-time systems.
Cuckoo hashing uses two or more hash functions and guarantees O(1) worst-case lookup by ensuring each key can be in only one of two positions. Insertion may require displacing existing keys and reinserting them in their alternate positions, potentially triggering a chain of displacements. While insertion can be O(n) in the worst case and may require a rehash, lookups are always at most two probes, making cuckoo hashing attractive for read-heavy workloads like network routers.
Hopscotch hashing and coalesced chaining attempt to combine the cache-friendliness of open addressing with the flexibility of chaining. Hopscotch hashing maintains a neighborhood of nearby buckets where a key is allowed to reside, ensuring that lookups only need to check a small, cache-resident window. Modern language runtimes often use even more specialized designs. Python's dict since version 3.6 uses a combined table that preserves insertion order while maintaining dense storage, and Java's HashMap converts long chains into balanced trees when a bucket exceeds 8 entries, improving worst-case from O(n) to O(log n).
Performance Tuning Techniques
Choosing the right initial capacity avoids repeated resizes if you know the approximate number of entries in advance. If you expect 10,000 entries and use a load factor of 0.75, initializing with capacity 13,334 prevents any resize. Using prime-number capacities can improve distribution for simple modulo hash functions, though power-of-two capacities allow faster bitwise AND instead of modulo when the hash function is well-mixed. For string keys, consider interning or using a faster hash like xxHash if profiling shows hashing is a bottleneck.
For concurrent environments, standard hash maps are not thread-safe. Options include concurrent hash maps that use lock striping, where the table is divided into segments each protected by its own lock, allowing parallel writes to different segments. Java's ConcurrentHashMap and Python's need for external locking or use of multiprocessing.Manager.dict illustrate this. Alternatively, lock-free hash maps using compare-and-swap operations can achieve higher throughput but are significantly more complex to implement correctly.
7. Real-World Applications
Hash maps are ubiquitous in systems programming. In web servers and caches, they power in-memory key-value stores like Redis and Memcached, where URL strings map to cached responses, session IDs map to user state, and rate-limiting counters map to IP addresses. A typical web application might use a hash map to store 100,000 active sessions, allowing O(1) lookup on every request without querying a database. Content Delivery Networks use hash maps to map content hashes to edge server locations for consistent hashing.
In databases, hash maps underpin hash indexes and hash joins. A hash index allows point lookups like SELECT * FROM users WHERE email = '[email protected]' to be answered without scanning a B-tree. During a hash join, the database builds a hash map from the smaller table's join keys, then probes it with keys from the larger table, achieving O(n + m) time instead of O(n log n) for sort-merge joins. Query optimizers also use hash maps for deduplication and grouping in GROUP BY operations.
Networking and operating systems rely heavily on hash maps for performance-critical lookups. Routers use hash tables to map IP prefixes to next-hop interfaces, firewalls map connection 4-tuples to state entries, and operating system kernels map file descriptors to file objects and process IDs to process control blocks. Compilers and interpreters use hash maps for symbol tables, mapping variable names to type information and memory locations during parsing and code generation.
| Operation | Description | Typical Use Case | Example |
|---|---|---|---|
| put(key, value) | Insert or update mapping | Cache, session store, frequency count | cache.put("user:42", "{name: 'Alice'}") |
| get(key) | Retrieve value by key | Lookup, authentication, routing | user = db.get("[email protected]") |
| remove(key) | Delete mapping | Cache eviction, logout, cleanup | sessions.remove(sessionId) |
| contains(key) | Check existence | Deduplication, set membership | if visited.contains(url): skip |
| iterate / keys() | Enumerate all entries | Serialization, analytics, rehashing | for k in map.keys(): persist(k) |
Beyond infrastructure, hash maps enable everyday application features. Spell checkers map words to correctness flags, autocomplete systems map prefixes to suggestion lists, and blockchain systems map transaction hashes to transaction data. In data science, hash maps implement the counting and grouping primitives that underlie MapReduce, where intermediate keys are shuffled to reducers via hashing.
8. Comparison with Alternatives
Choosing a data structure requires understanding trade-offs. Hash maps excel at unordered key-value lookup but are not always the best choice when ordering, memory efficiency, or worst-case guarantees matter. The table below provides a comprehensive comparison to guide selection.
| Feature | Hash Map | Balanced BST (AVL / Red-Black Tree) | Sorted Array | Linked List / Unsorted Array |
|---|---|---|---|---|
| Search Time | O(1) avg, O(n) worst | O(log n) guaranteed | O(log n) via binary search | O(n) |
| Insert Time | O(1) amortized avg, O(n) worst | O(log n) | O(n) due to shifting | O(1) at head, O(n) search |
| Delete Time | O(1) avg, O(n) worst | O(log n) | O(n) due to shifting | O(n) |
| Ordered Iteration | No (unless ordered variant) | Yes, sorted by key | Yes, sorted | No, insertion order only |
| Range Queries | Not efficient | Efficient O(log n + k) | Efficient O(log n + k) | O(n) |
| Memory Overhead | High (buckets + load factor) | Moderate (pointers per node) | Low (contiguous storage) | Low to moderate |
| Cache Friendliness | Moderate (open addressing is better) | Poor (pointer chasing) | Excellent | Poor for linked list |
| Worst-Case Guarantee | No, degrades with collisions | Yes, O(log n) | Yes, O(log n) search | No, always O(n) search |
| Key Requirements | Hashable, good hash function | Comparable / ordered | Comparable / ordered | Equality check only |
Use a hash map when you need fast point lookups, insertions, and deletions without ordering requirements. Examples include dictionaries, caches, and frequency tables. Choose a balanced BST when you need ordered traversal, range queries like "find all keys between A and B", or guaranteed logarithmic worst-case. Sorted arrays are ideal when data is static and you need binary search with minimal memory and excellent cache locality. Linked lists are rarely used for lookup but remain useful for sequential access and when insertion order must be preserved with minimal overhead.
Hybrid structures also exist. Python's OrderedDict and Java's LinkedHashMap combine hash maps with linked lists to preserve insertion order while retaining O(1) lookup. Tree-based maps like Java's TreeMap provide sorted hash-map-like APIs. Understanding these alternatives prevents overusing hash maps where a simpler or more ordered structure would be clearer and more efficient.
9. Common Mistakes and Best Practices
One of the most frequent errors is using mutable keys. In Python, if you use a list as a dictionary key and then modify the list, its hash changes, making the entry unreachable and corrupting the table. Always use immutable, hashable types for keys: strings, numbers, tuples of immutables, or custom objects that implement __hash__ and __eq__ correctly and immutably. If you must use a custom object as a key, ensure its hash is computed from immutable fields and that __eq__ is consistent with __hash__.
Another pitfall is implementing a poor hash function that causes excessive collisions. A classic mistake is hashing strings by summing character codes, which makes anagrams collide, or returning a constant for all keys, which degrades the map to a linked list. Always test your hash function with realistic data distributions. Use established algorithms and ensure that if a == b then hash(a) == hash(b), but also strive for hash(a) != hash(b) when a != b. For cryptographic or security-sensitive contexts, use a randomized seed to prevent hash-flooding denial-of-service attacks where an attacker crafts keys that all collide.
Ignoring load factor and initial capacity leads to performance surprises. Creating a hash map with default capacity 16 and then inserting 1,000,000 entries will trigger many expensive resizes. If you know the expected size, pre-size the map. Conversely, setting the load factor too high, like 0.95, saves memory but increases collisions and probe lengths, hurting latency. The default 0.75 is a well-tested balance for general use. Also, avoid storing None as a sentinel for empty slots if None is a valid key or value; use a dedicated sentinel object as shown in the open addressing example.
Best Practices Checklist
- Make keys immutable and ensure
__hash__and__eq__are consistent and based on immutable state. - Choose an appropriate initial capacity to minimize resizes when the size is predictable.
- Keep load factor around 0.7–0.75 for chaining, lower (0.5–0.7) for open addressing.
- Use prime or power-of-two capacities appropriately and test distribution with real data.
- Handle deletions correctly with tombstones for open addressing; do not simply clear slots.
- Consider thread safety: use concurrent hash map variants or external synchronization for multi-threaded access.
- Profile before optimizing: measure collision rates and resize frequency rather than assuming bottlenecks.
Finally, remember that hash maps do not preserve order unless explicitly designed to do so. Relying on iteration order of a standard hash map is a bug waiting to happen, as order can change after resizes or across language versions. If order matters, use an ordered variant or maintain a separate list of keys. Document whether your map is ordered to prevent future misuse.
10. Interview Questions
Hash maps are a staple of technical interviews because they test understanding of hashing, complexity, and practical trade-offs. Below are common questions with guidance on how to approach them.
Question 1: Design a Hash Map without using built-in libraries
Interviewers often ask you to implement MyHashMap with put, get, and remove methods. The expected solution is to explain your choice of collision resolution, typically separate chaining for simplicity, define a hash function, handle resizing, and analyze complexity. Be prepared to code the structure on a whiteboard, discuss handling of collisions, and explain why you chose a particular load factor and capacity growth strategy. Mention edge cases like duplicate keys, deletion, and rehashing.
Question 2: Why is the worst-case time O(n) and how would you mitigate it?
This tests understanding of collisions. Answer by explaining that if all keys hash to the same bucket, the chain length becomes n, making operations linear. Mitigations include using a better hash function, keeping load factor low, using a balanced tree for long chains as Java 8 does, or using universal hashing with randomization. Discuss the difference between average-case O(1) under simple uniform hashing and worst-case O(n), and why average-case is what matters in practice if the hash function is good.
Question 3: Find the first non-repeating character in a string
Given a string like "swiss", find the first character that appears exactly once. The optimal solution uses a hash map to count frequencies in one pass O(n), then a second pass to find the first character with count 1. This demonstrates using a hash map for frequency counting, a pattern that appears in many problems like two-sum, group anagrams, and longest substring without repeating characters. Be ready to discuss space complexity O(k) where k is alphabet size, and alternative approaches.
Question 4: How would you implement an LRU Cache?
An LRU (Least Recently Used) Cache requires O(1) get and put with eviction of the least recently used item when capacity is exceeded. The classic solution combines a hash map with a doubly linked list. The hash map maps keys to list nodes for O(1) lookup, while the doubly linked list maintains usage order, moving accessed nodes to the front and evicting from the tail. This question tests ability to combine data structures and handle pointer manipulation, and is frequently asked at top technology companies.
Question 5: Difference between HashMap and HashSet, and when to use each?
A HashSet is essentially a HashMap where only keys matter and values are dummy placeholders. Use a HashSet for membership testing like deduplication or visited sets in graph traversal, and a HashMap when you need to associate data with each key. Explain that both rely on hashing and share the same complexity, but HashSet can be more memory-efficient for set operations. Be prepared to discuss implementation: a HashSet can be built on top of a HashMap by storing keys with a constant value.
Mastering these questions requires not just memorizing answers but understanding the underlying principles of hashing, collision resolution, and complexity analysis. Practice implementing a hash map from scratch, analyze its behavior under different load factors, and solve at least 10-15 LeetCode problems tagged with hash table to build fluency.
Frequently Asked Questions
What is Hash Map and how does it work?
Hash Map is a fundamental concept in computer science. It involves specific techniques and data organization methods designed to solve computational problems efficiently. Understanding the core mechanics helps developers choose the right approach for their applications.
What is the time complexity of Hash Map?
The time complexity of Hash Map varies depending on the specific operation and implementation. Common complexities range from O(1) constant time for basic operations to O(n log n) or O(n²) for more complex operations. The article above provides detailed complexity analysis for each operation.
When should I use Hash Map in my projects?
Use Hash Map when your application requires the specific performance characteristics it provides. Consider factors like data size, operation frequency, memory constraints, and whether the problem domain matches the strengths of Hash Map. The real-world applications section above covers common use cases.
What are the advantages and disadvantages of Hash Map?
The main advantages of Hash Map include efficient performance for its target use cases and well-understood theoretical guarantees. Disadvantages may include implementation complexity, memory overhead, or performance limitations in certain edge cases. Always benchmark against alternatives for your specific workload.
How does Hash Map compare to its alternatives?
Hash Map offers distinct trade-offs compared to alternative approaches. While it excels in certain scenarios, other algorithms or data structures may be better suited for different problem constraints. The comparison table in the article provides a detailed analysis of these trade-offs.