Hash Maps and Sets

Topics Covered

Hash Map Fundamentals

Why O(1) Changes Everything

How Hashing Works Under the Hood

Hash Collisions

Load Factor and Rehashing

Worst-Case Performance and HashDoS

When NOT to Use a Hash Map

Language-Specific Implementations

Hash Map vs. Other Data Structures - a Decision Framework

Interview Strategy: Default to Hash Map, Then Optimize

Two Sum Pattern

The Brute Force and Why It Fails

The Hash Map Insight

Why Single-Pass Works

Extending to 3Sum and 4Sum

The "Store What You Have Seen" Principle

Recognizing Two Sum Variants in Disguise

Time-Space Trade-Off: The Core Lesson

Grouping and Bucketing

The Anagram Grouping Problem

Choosing the Right Key

Building Adjacency-Like Structures

When the "Group By" Pattern Applies

Practical Pitfalls in Key Design

Real-World Applications of Grouping

Counting and Frequency Maps

Why Frequency Maps Are So Common in Interviews

Building a Frequency Map

First Unique Character

Top-K Frequent Elements

Anagram Checking with Frequency Maps

Sliding Window Frequency Maps

Optimizing Frequency Map Comparisons

When Frequency Maps Combine with Other Structures

Set Operations for Deduplication

Sets for O(1) Membership Testing

Deduplication

Set Intersection, Union, and Difference

The Longest Consecutive Sequence Pattern

When to Use a Set vs. a Sorted Array

Common Set Patterns in Interviews

Summary: Choosing Between Hash Map and Set

Practice

The single most important data structure for coding interviews is the hash map. Not because it is clever or exotic - but because it turns an entire class of slow problems into fast ones. If your brute-force solution involves scanning an array for every element, a hash map almost certainly eliminates that inner scan.

Why O(1) Changes Everything

A hash map gives you average-case O(1) lookup, insertion, and deletion. Compare that to the alternatives:

OperationArray (unsorted)Sorted ArrayHash Map
SearchO(n)O(log n)O(1)
InsertO(1) appendO(n) shiftO(1)
DeleteO(n)O(n) shiftO(1)

That O(n) search in an unsorted array is the root cause of most O(n^2) brute-force solutions. When you have an outer loop iterating through n elements and an inner loop searching for a match, the total cost is O(n) times O(n). Replace the inner search with a hash map lookup and the entire algorithm drops to O(n) times O(1) = O(n).

This is not a trick - it is the fundamental pattern behind dozens of interview problems. Two Sum, finding duplicates, checking if an anagram exists, grouping elements by a property - all of them follow the same structure: replace a linear scan with a constant-time lookup.

How Hashing Works Under the Hood

A hash map stores key-value pairs in an internal array of "buckets." When you insert a key, the hash function converts the key into an integer, and that integer modulo the array size determines which bucket holds the entry.

The hash function must be deterministic - the same key always produces the same hash. It should also distribute keys uniformly across buckets. A bad hash function that maps many keys to the same bucket degrades performance from O(1) to O(n) because that bucket becomes a long list you must scan linearly.

Hash Collisions

Two different keys can hash to the same bucket index. This is called a collision, and every hash map implementation must handle it. There are two main strategies:

Chaining stores a linked list (or small array) at each bucket. When a collision occurs, the new entry is appended to the list. Lookup scans the list at the target bucket for the matching key. Python's dict and Java's HashMap use chaining (Java switches from linked list to a balanced tree when a single bucket exceeds 8 entries).

Open addressing stores all entries directly in the bucket array. When a collision occurs, the algorithm probes subsequent slots (linear probing, quadratic probing, or double hashing) until it finds an empty one. Lookup follows the same probe sequence. This approach has better cache performance because entries are contiguous in memory, but it degrades faster as the table fills up.

Interview Tip

You will rarely need to implement a hash map from scratch in an interview. But understanding collisions explains why hash map operations are O(1) on average, not guaranteed. A degenerate hash function or adversarial input can force all keys into one bucket, making every operation O(n). This is the worst case - and it is why interviewers sometimes ask about it.

Load Factor and Rehashing

The load factor is the ratio of stored entries to total buckets. As the load factor increases, collisions become more frequent and performance degrades. Most implementations set a threshold (0.75 for Java's HashMap, around 0.66 for Python's dict). When the load factor exceeds this threshold, the hash map allocates a larger array (typically double the size) and rehashes every entry into the new array.

Rehashing is O(n) - but it happens infrequently. Amortized across all insertions, the cost per insertion remains O(1). This is the same amortization argument that makes dynamic array (ArrayList) appends O(1) amortized despite occasional O(n) resizing.

If you know the number of entries in advance, you can pre-size the hash map to avoid rehashing entirely. In Java: new HashMap<>(expectedSize * 4 / 3) accounts for the 0.75 load factor. In Python: dicts resize automatically and you cannot pre-size, but dict.fromkeys(keys) is optimized for bulk insertion.

Worst-Case Performance and HashDoS

Because hash map performance depends on the quality of the hash function, an adversary who knows the hash function can craft inputs where all keys collide into the same bucket. This is called a HashDoS attack. Every operation degrades to O(n), and a server processing thousands of such requests per second becomes unresponsive.

Defenses include: randomized hash seeds (Python does this by default since Python 3.3), switching to balanced trees for overloaded buckets (Java 8+ HashMap), and limiting the number of keys accepted from untrusted input.

For interview purposes, when asked about worst-case complexity, mention that hash map operations are O(1) average and O(n) worst case. The worst case is pathological and unlikely with a good hash function, but acknowledging it shows depth of understanding. In practice, hash maps behave as O(1) for virtually all real-world inputs.

When NOT to Use a Hash Map

Hash maps are not universally better than alternatives. Recognize these situations:

You need sorted order. Hash maps do not maintain any ordering of keys. If you need the minimum key, the maximum key, or keys in a range, use a balanced BST (TreeMap in Java, SortedDict from sortedcontainers in Python). A hash map would require sorting all keys every time - O(n log n) versus O(log n) for a tree.

Memory is severely constrained. Hash maps use more memory than arrays. Each entry has overhead for the key's hash, pointers in the collision chain, and empty buckets reserved for future entries. For large datasets on memory-constrained systems, a sorted array with binary search uses less total memory.

Keys are not hashable. Mutable objects like lists and dictionaries cannot be used as hash map keys in Python because their hash would change when they are modified, breaking the data structure. You must convert them to immutable equivalents (tuples, frozensets) first.

Language-Specific Implementations

Different languages name their hash maps differently, but the underlying mechanics are the same:

LanguageHash MapHash Set
Pythondict, defaultdict, Counterset, frozenset
JavaHashMap, LinkedHashMap, TreeMapHashSet, LinkedHashSet, TreeSet
JavaScriptMap, plain objects {}Set
C++unordered_mapunordered_set
Gomap[K]Vmap[K]bool (no built-in set)

Python's dict is the most interview-friendly - clean syntax, O(1) operations, and built-in methods like .get(key, default) that eliminate boilerplate. Java's HashMap requires more ceremony but offers getOrDefault, computeIfAbsent, and merge methods that simplify frequency counting and grouping.

Hash Map vs. Other Data Structures - a Decision Framework

When choosing a data structure, think about the operations you need most frequently:

  • Need fast lookup by key? Hash map.
  • Need fast lookup AND sorted iteration? Balanced BST (TreeMap, SortedDict).
  • Need fast lookup AND insertion order? LinkedHashMap in Java or regular dict in Python 3.7+ (which preserves insertion order).
  • Need to count occurrences? Hash map (or Counter in Python).
  • Need only membership testing, no associated values? Hash set.

The hash map is the default "fast lookup" structure. You should only reach for alternatives when the problem requires something a hash map cannot provide - primarily sorted order or range queries.

Interview Strategy: Default to Hash Map, Then Optimize

When you see a problem for the first time in an interview, a reliable approach is:

  1. Start with brute force. Write out the O(n^2) or O(n * m) solution. This shows the interviewer you understand the problem.
  2. Identify the repeated work. Which part of the brute force is doing the same computation over and over? Usually it is a search or a comparison.
  3. Replace the repeated work with a hash map lookup. Store results of previous computations so future iterations can query them in O(1).
  4. Verify correctness. Walk through a small example to confirm the hash map solution produces the same output as brute force.
  5. Analyze time and space. The new solution is typically O(n) time and O(n) space.

This five-step approach works for the majority of hash-map-solvable problems. The hard part is step 2 - recognizing what to store. The rest is mechanical. Practice this sequence on 10-15 hash map problems and it becomes instinctive - you will start jumping from step 1 to step 3 without conscious effort.

Two Sum is the most famous interview problem for a reason - it distills the core hash map insight into its purest form. Every element you have already seen becomes ammunition for solving the rest of the problem. This "store what you've seen" principle extends far beyond Two Sum into dozens of other problems.

The Brute Force and Why It Fails

Given an array of integers and a target sum, find two elements that add up to the target. The naive approach checks every pair:

For every element, you scan the rest of the array for its complement. With n elements, that is roughly n^2/2 comparisons. For an array of 10,000 elements, that is 50 million comparisons. For a million elements, that is 500 billion comparisons. Even on modern hardware executing billions of operations per second, the brute force takes minutes. We need a fundamentally different approach.

The Hash Map Insight

The key realization: for each element nums[i], you do not need to search forward for target - nums[i]. You only need to check whether you have already seen it. A hash map stores every element you have visited, keyed by its value and pointing to its index.

One pass through the array. For each element, compute the complement, check the map, and if not found, store the current element. The map grows as you iterate, so by the time you reach element k, the map contains all elements from index 0 to k-1. If a valid pair exists, the second element of the pair will find the first already in the map.

Two Sum with hash map
lookupstorefound!nums (target = 9)271115Hash Map{ }complement?Match!2 + 7 = 9
Each element is checked against the hash map for its complement. When a match is found, the pair is returned.

Step through the algorithm interactively - watch how the hash map grows and the complement lookup finds the match:

Loading animator...

Why Single-Pass Works

A common question: do you need two passes - one to build the map, one to query it? No. If elements at indices i and j form a pair (where i < j), then when you reach index j the element at index i is already in the map. You will always encounter the earlier element first, store it, and find it when the later element looks for its complement.

The only edge case: can an element pair with itself? If the problem says "use each element at most once," the single-pass approach handles this naturally - you check the map before inserting the current element, so an element never finds itself.

Interview Tip

When you see a problem asking 'find two elements that satisfy some relationship,' your first instinct should be: can I rephrase this as 'for each element, does its partner already exist in a hash map?' This reframing turns O(n^2) pair-search into O(n) single-pass in a huge number of problems.

Extending to 3Sum and 4Sum

3Sum asks for three elements that sum to zero. The standard approach sorts the array first, then for each element, runs a two-pointer scan on the remaining elements. But the hash map version works too: for each pair (i, j), check if -(nums[i] + nums[j]) exists in the map. The key difference is that 3Sum requires deduplication - sorting helps with that because identical values are adjacent and easy to skip.

4Sum extends the same idea - fix one element, reduce to 3Sum. Each additional element adds a factor of O(n), so 4Sum is O(n^3). The pattern generalizes: k-Sum is O(n^(k-1)) with sorting and two pointers for the innermost pair.

The "Store What You Have Seen" Principle

Two Sum is a specific instance of a broader strategy: as you iterate through data, build a map of what you have already processed, so future elements can query past elements in O(1). This principle applies to:

  • Subarray sum equals K - store prefix sums in a map. For each new prefix sum, check if current_prefix - K exists.
  • Longest substring without repeating characters - store the last index of each character. When a repeat is found, jump the window start forward.
  • Finding duplicates within distance K - store each element's most recent index. If the same value appears within K indices, you have a duplicate.
  • Two Sum variants - Two Sum with sorted input uses two pointers, Two Sum with a BST uses in-order traversal, and Two Sum with a stream uses the hash map approach since you cannot sort an infinite stream.

The unifying idea is the same: convert "search backward through what I have seen" into "look up in a map."

Recognizing Two Sum Variants in Disguise

Many problems are Two Sum wearing a different costume. The pattern to watch for: you need to find two pieces of data whose combination satisfies a condition, and you can compute what the "missing piece" would be.

Two Sum with sorted input - instead of a hash map, use two pointers from both ends. If the sum is too small, advance the left pointer. If too large, retreat the right pointer. O(n) time, O(1) space. The trade-off: you lose index information if the problem asks for original indices (sorting changes positions).

Two Sum in a BST - given a balanced BST and a target, check if any two nodes sum to the target. Use in-order traversal to get a sorted array, then apply two pointers. Or iterate with a set: for each node value, check if target - value is in the set.

Pair with given difference - find two elements whose difference equals K. For each element a, check if a + K or a - K exists in the map. Same hash map technique, different arithmetic.

Once you internalize the Two Sum pattern, you start seeing it everywhere. The specific relationship changes (sum, difference, XOR, product), but the structure is identical: iterate, compute what you need, check the map.

Time-Space Trade-Off: The Core Lesson

Two Sum is the cleanest illustration of the most important trade-off in algorithm design: you can often trade space for time. The brute-force solution uses O(1) extra space but O(n^2) time. The hash map solution uses O(n) extra space but O(n) time. In most real-world systems, memory is cheap and latency is expensive. An extra megabyte of RAM to avoid 50 million comparisons is a good deal.

This trade-off appears throughout algorithms:

  • Memoization trades O(n) space for exponential-to-polynomial time reduction in dynamic programming.
  • Caching trades storage space for reduced computation on repeated queries.
  • Precomputed lookup tables trade O(n) space for O(1) query time.

Hash maps are the Swiss army knife for this trade-off. Whenever you find yourself doing repeated work (scanning, searching, recomputing), ask: can I precompute and store results in a hash map so that future lookups are instant?

In an interview setting, explicitly stating this trade-off earns points. Say: "The brute-force approach uses O(1) space but O(n^2) time. I can reduce time to O(n) by using O(n) extra space for a hash map." This tells the interviewer you understand the fundamental trade-off and are making a conscious engineering decision, not just memorizing a trick.

Many problems ask you to partition elements into groups based on some shared property. Without a hash map, grouping requires sorting (O(n log n)) or nested loops (O(n^2)). With a hash map, you compute a key for each element and drop it into the right bucket in O(1). Total cost: O(n).

The Anagram Grouping Problem

The classic example: given a list of strings, group all anagrams together. "eat," "tea," and "ate" are anagrams - they contain the same characters in different order.

The insight is that all anagrams share the same canonical form. If you sort the characters of each word, anagrams produce identical sorted strings. "eat" becomes "aet," "tea" becomes "aet," "ate" becomes "aet." Use this sorted string as the hash map key, and each key's value is the list of original words that map to it.

Anagram grouping by sorted key
Wordseatteatanatenatbatsort()"aet"eat, tea, ate"ant"tan, nat"abt"bat
Each word is sorted to produce a canonical key. Words with the same key land in the same bucket.

See the grouping in action - watch how the sorted-character key routes each word to the correct bucket:

Loading animator...

Choosing the Right Key

The key you compute determines what "same group" means. Different problems demand different key functions:

Sorted characters for anagram grouping - sorted("tea") gives "aet".

Character frequency tuple for anagram grouping without sorting - count the frequency of each letter and use the tuple of 26 counts as the key. This runs in O(k) per word (where k is word length) instead of O(k log k) for sorting.

Digit root or modular arithmetic for grouping numbers by a mathematical property.

String pattern for grouping words with the same structure. "abb" and "zoo" have the same pattern (one unique character followed by two of another). Encode the pattern as a tuple of first-occurrence indices.

Building Adjacency-Like Structures

Grouping is not limited to flat partitions. Hash maps build graph-like structures when the key represents a relationship. For example, given a list of prerequisite pairs [[course, prereq], ...], build an adjacency list:

This is O(n) to build and O(1) to query - "which courses depend on course X?" is a single hash map lookup. Without a hash map, answering that question requires scanning all pairs every time.

When the "Group By" Pattern Applies

Look for these signals in a problem statement:

  • "Group elements that share property X" - direct grouping.
  • "Find all elements equivalent to each other" - equivalence classes, each class is a bucket.
  • "Build a mapping from X to all Y values associated with X" - adjacency list or index.
  • "Partition the input based on a computed property" - the property is the key.

The implementation is always the same: iterate once, compute a key for each element, and append the element to map[key]. The variation is entirely in how you compute the key.

Interview Tip

If you find yourself writing nested loops to compare every pair of elements for some equivalence relation, stop and ask: can I compute a canonical form that is identical for equivalent elements? If yes, group by that canonical form using a hash map. This converts O(n^2) pairwise comparison into O(n) single-pass grouping.

Practical Pitfalls in Key Design

Unhashable keys. In Python, lists are not hashable, so you cannot use [0, 1, 1] as a dict key. Convert to tuple([0, 1, 1]) or use str([0, 1, 1]). In Java, use Arrays.toString(count) or build a string key from the frequency array.

Key collisions across groups. Your key function must produce the same key for all and only the elements in the same group. If two non-equivalent elements produce the same key (a false collision), they get grouped incorrectly. For anagram grouping, sorting characters is collision-free because two strings with the same sorted form are by definition anagrams. But using only the first letter as a key would incorrectly group "apple" with "ant."

Performance of key computation. The total cost of grouping is O(n * cost_per_key). If your key function is expensive (e.g., computing a hash of a large file), the key computation dominates. Optimize the key function for the expected input size.

Real-World Applications of Grouping

Grouping is not just an interview pattern - it is a fundamental data-processing operation:

  • SQL GROUP BY is exactly this pattern applied to database rows. The query engine groups rows by the specified columns and applies aggregate functions to each group.
  • MapReduce frameworks partition intermediate data by key (the "shuffle" phase), so that all values for a given key are processed by the same reducer.
  • Log analysis groups log entries by error type, user ID, or time window to compute metrics like error rates and user activity.

When you implement grouping in code, you are doing what databases and distributed systems do at scale - partitioning data by a computed key and processing each partition independently.

The lesson for interviews: if a problem mentions "group," "partition," "categorize," or "classify," your first instinct should be a hash map where the key is the grouping criterion and the value is a list of grouped items. Write the generic pattern, then focus your effort on designing the right key function - that is where the problem-specific insight lives.

A frequency map counts how many times each element appears. This simple structure powers a surprising number of interview problems - from finding the first unique character to solving Top-K questions to checking whether two strings are anagrams.

Why Frequency Maps Are So Common in Interviews

Frequency maps show up everywhere because they reduce a category of questions to a common template. "How many times does X appear?" is the fundamental question, and the answers enable: finding duplicates (count > 1), finding unique elements (count == 1), checking permutations (identical frequency maps), and ranking by popularity (sort by count). Learn the frequency map pattern once and you unlock all of these.

Building a Frequency Map

The pattern is always the same: iterate through the collection, and for each element, increment its count in the map.

Time: O(n). Space: O(k) where k is the number of distinct elements. In the worst case (all elements are unique), k = n. In practice, for problems with bounded input (like lowercase English letters), k is at most 26 regardless of input size.

In Java, use HashMap<T, Integer> with getOrDefault. Switch to the Java tab above to see the idiomatic approach.

First Unique Character

Given a string, find the first character that appears exactly once. Build a frequency map in one pass, then iterate through the string again and return the first character with count 1.

Why two passes? The frequency map tells you which characters are unique but not their positions. The second pass checks characters in order of appearance, so the first unique one found is the answer. Total time: O(n) + O(n) = O(n).

A variation: if you need the last unique character, iterate the string in reverse on the second pass. If you need all unique characters in order, collect them during the second pass instead of returning the first one. The frequency map stays the same - only the second pass changes based on what the problem asks for.

Frequency map and Top-K extraction
countpushElementsabacabFreq Mapa: 3b: 2c: 1Top-2Min-Heap(size K=2)a (3)b (2)
Elements are counted into a frequency map, then the K most frequent are extracted via a min-heap.

Top-K Frequent Elements

Given an array and an integer K, return the K most frequent elements. This is a two-step problem: (1) build a frequency map, (2) extract the top K.

Heap approach (O(n log K)): Build the frequency map in O(n). Then use a min-heap of size K. For each element-count pair, push onto the heap. If the heap exceeds size K, pop the smallest. At the end, the heap contains the K most frequent elements.

Step through the heap approach - watch elements enter the min-heap and see how it maintains only the top K:

Loading animator...

Bucket sort approach (O(n)): Create an array of buckets where index i holds all elements that appear exactly i times. The array length is n+1 (an element can appear at most n times). Walk the buckets from right to left and collect elements until you have K.

The bucket sort approach is O(n) because both building the frequency map and scanning the buckets are linear. This is optimal when K could be large relative to n.

Anagram Checking with Frequency Maps

Two strings are anagrams if and only if they have identical character frequencies. Build a frequency map for each string and compare:

Or use a single map - increment for characters in s, decrement for characters in t. If all counts are zero at the end, the strings are anagrams:

Sliding Window Frequency Maps

When you need to check a property of every subarray or substring of fixed length K, maintain a frequency map that updates as the window slides. Add the new element entering the window, remove the element leaving it. Each slide is O(1), so the entire sweep is O(n).

This sliding window frequency map is a powerful technique - it turns "check all substrings of length K" from O(n*K) (rebuilding the map each time) to O(n) (incrementally updating it).

Optimizing Frequency Map Comparisons

Comparing two Counter objects (or two HashMaps) is O(k) where k is the number of distinct keys. For the sliding window anagram problem, this comparison happens at every position. You can optimize further by maintaining a count of how many characters have the "correct" frequency. When this count equals the number of distinct characters in the pattern, you have a match - converting the comparison from O(k) per step to O(1) per step.

The exact implementation details vary, but the core idea is the same: track how many frequency entries match rather than comparing the entire maps every step.

When Frequency Maps Combine with Other Structures

Frequency maps rarely solve problems in isolation - they feed into other algorithms:

  • Frequency map + heap = Top-K problems (most frequent elements, least frequent elements)
  • Frequency map + bucket array = O(n) Top-K via bucket sort
  • Frequency map + sliding window = substring/subarray pattern matching
  • Two frequency maps + comparison = anagram verification, permutation checking
  • Frequency map + greedy = task scheduling (pick the most frequent task next)

Recognizing which combination a problem requires is often the hardest part. The implementation of each piece is straightforward once you identify the structure.

A useful interview technique: when you see a problem involving counts or frequencies, immediately build a frequency map as step one. Even before you know the full algorithm, the frequency map gives you a structured view of the data that makes subsequent steps clearer. Many candidates who struggle with frequency-based problems do so because they try to count while simultaneously solving - separating the counting step from the logic step simplifies both.

A set is a hash map without values - it stores keys only, giving you O(1) membership testing, insertion, and deletion. When you do not need to associate data with each key and only care about "is this element present?", a set is the cleaner and more memory-efficient choice.

Sets for O(1) Membership Testing

The most basic use: checking whether an element exists in a collection. Arrays require O(n) scanning. Sorted arrays need O(log n) binary search. Sets do it in O(1).

This matters when you perform many membership checks. If you need to check 1,000 elements against a collection of 100,000, using a list costs O(1000 * 100000) = O(10^8). Using a set costs O(1000 * 1) = O(1000). The set approach is 100,000 times faster.

Deduplication

Removing duplicates from a collection is a one-liner with sets:

If order matters, use a set to track what you have seen while preserving insertion order:

This runs in O(n) time and O(n) space. The alternative - checking each element against all previous elements - is O(n^2).

In Python 3.7+, regular dictionaries preserve insertion order, so dict.fromkeys(items) is an alternative that deduplicates while preserving order in a single expression:

In Java, LinkedHashSet combines set semantics with insertion-order iteration - it is the equivalent tool for order-preserving deduplication.

Set Intersection, Union, and Difference

Sets support mathematical operations that solve comparison problems concisely:

Interview application: "Find common elements between two arrays" is set intersection. "Find elements in array A that are not in array B" is set difference. These operations are O(n + m) where n and m are the set sizes - significantly faster than the O(n * m) nested loop approach.

In Java, the same operations work through the Set interface using retainAll() (intersection), addAll() (union), and removeAll() (difference). Switch to the Java tab above to see the full syntax. Note that Java's set operations modify the set in place, so always copy first if you need to preserve the original.

Time complexity of set operations: Intersection of two sets with sizes n and m takes O(min(n, m)) - iterate through the smaller set and check membership in the larger. Union takes O(n + m). Difference takes O(n). These bounds assume O(1) hash lookups, which holds on average for well-distributed hashes.

Set intersection
Set A1357Set B2358in B?A ∩ B351 ✗7 ✗
Elements from set A are checked for membership in set B. Matches form the intersection.

The Longest Consecutive Sequence Pattern

This problem demonstrates the power of set-based thinking: given an unsorted array, find the length of the longest consecutive sequence (e.g., [100, 4, 200, 1, 3, 2] has longest sequence [1, 2, 3, 4] with length 4).

Sorting gives O(n log n). A set-based approach achieves O(n):

Walk through the algorithm step by step - notice how only sequence-start elements trigger the counting loop:

Loading animator...

The key insight: only start counting from the beginning of a sequence - an element whose predecessor (num - 1) is not in the set. This check ensures each element is visited at most twice (once in the outer loop, once in the inner while loop), keeping the total work O(n).

Without the num - 1 check, you would start a new sequence from every element, and the inner while loop would count up the same sequence multiple times - O(n^2) in the worst case.

Why a set and not a hash map? You only need to answer "does this number exist?" - there is no associated value. A set is the natural choice. You could use a hash map with dummy values, but the set communicates intent more clearly and uses slightly less memory.

Handling duplicates. The set automatically deduplicates the input. If the array contains [1, 1, 2, 2, 3], the set is {1, 2, 3}, and the algorithm finds the sequence of length 3 without being confused by repeated values.

Why not use a hash map instead of sorting? You could store values in a hash map and walk chains from each potential start. But a set is simpler - you do not need values, only membership. And the set-based O(n) solution is strictly better than the O(n log n) sorting approach when the input should not be modified (sorting changes the array, or requires copying it first). In an interview, if asked "can you do better than O(n log n)?", the set-based approach is the answer.

Interview Tip

The longest consecutive sequence trick is a general pattern: use a set for O(1) 'does the neighbor exist?' checks, and add a guard condition that prevents redundant work. Look for similar opportunities whenever a problem involves finding connected runs, chains, or sequences in unsorted data.

When to Use a Set vs. a Sorted Array

CriteriaSetSorted Array
Membership testO(1) averageO(log n)
Insert/DeleteO(1) averageO(n) shift
Range queryNot supportedO(log n + k)
Ordered iterationNot supportedO(n)
Memory overheadHigher (hash table)Lower (contiguous)
Cache performanceWorse (pointer chasing)Better (contiguous)

Use a set when you need fast membership testing and modification with no ordering requirements. Use a sorted array when you need range queries, ordered traversal, or memory efficiency on static data. If you need both fast lookup and ordering, consider a balanced BST (TreeMap/TreeSet in Java, SortedList from sortedcontainers in Python).

Common Set Patterns in Interviews

Cycle detection in linked lists - use a set to track visited nodes. If you visit a node already in the set, there is a cycle. (Floyd's algorithm is more space-efficient, but the set approach is simpler to implement.)

Finding the missing number - given numbers 1 to n with one missing, put them in a set and check which number from 1 to n is absent. O(n) time and space. (Math-based approaches use O(1) space, but the set approach generalizes to multiple missing numbers.)

Valid Sudoku - for each row, column, and 3x3 box, use a set to check for duplicates. If any insertion finds the element already present, the board is invalid.

Contains duplicate within K distance - as you iterate, maintain a set of the last K elements. Before adding the current element, check if it is already in the set. After adding, if the set exceeds size K, remove the element that entered K steps ago. This sliding window set gives O(n) time with O(K) space.

Summary: Choosing Between Hash Map and Set

The decision is simple: if you need to associate each key with a value (index, count, list of grouped items), use a hash map. If you only need to answer "does this key exist?", use a set. Sets are lighter-weight - less memory, cleaner code - so default to sets when values are unnecessary. You can always upgrade to a hash map later if the problem evolves to require associated data.

One final tip: in interviews, when you reach for a set, say "I will use a set since I only need membership testing, not key-value pairs." This small detail signals to the interviewer that you are choosing data structures deliberately rather than defaulting to whatever comes to mind first.

Loading problem...

Loading problem...

Loading problem...