Arrays & Hashing Problems
Seven classic interview problems — Two Sum, Contains Duplicate, Valid Anagram, Group Anagrams, Top K Frequent Elements, Product of Array Except Self and Longest Consecutive Sequence — solved from zero. For each one you will see the slow obvious idea, the one observation that makes it fast, the pseudocode, the Dart code, and an animation you can feed your own input. By the end you will recognise the "remember what you have seen" pattern in a new problem and know which tool (a Set, a Map, a counting array, or sorting) to reach for.
How to attack an array or hashing problem
A few words first, because every problem below uses them:
- An array (in Dart, a
List) is a row of numbered boxes. Box numbers (the index) start at 0. Reading box 5 is instant, but finding which box holds the value 7 means looking at the boxes one by one. - A hash set (Dart
Set) is a bag of values that answers "is x in here?" in about one step on average. It does that by turning x into a box number with a hash function (a recipe that maps a value to a box), so it can jump straight to the right box. How that works inside is the subject of C11 · Hash Tables. - A hash map (Dart
Map) is the same idea with a value attached to each key: "7 → index 3", "apple → 2 times". Every built-in method and its cost is in D27 · Set & Map methods. - O(n) ("order n") means the work grows in step with the input size n: twice the input, about twice the work. O(n²) means twice the input gives four times the work. O(1) means the work does not grow at all. The full story is in C03 · Growth of Functions.
The four-step plan
- Write the brute force first. The obvious idea, usually "try every pair" or "try everything". It is often too slow, but it proves you understand the task and gives you a reference to test against.
- Spot the repeated work. Ask: "what question does my inner loop keep asking?" In Two Sum the inner loop keeps asking "is my partner somewhere in this list?" — and it answers by scanning, again and again.
- Trade memory for time. Store the answers to that repeated question in a
SetorMapas you go, so each later question costs one lookup instead of a scan. This usually turns O(n²) into O(n) time, at the price of O(n) extra memory. - Check the edges. Empty input, one element, duplicates, negative numbers, very large values. Most wrong answers in interviews come from these, not from the main idea.
Here is step 1 for Two Sum (find two positions whose values add up to a target). Watch how often the same nums[i] is re-read while j walks right — that is the repeated work.
Read the constraints: they tell you the speed you need
Interview problems state input sizes. A typical computer does roughly 108 simple steps per second, so the size of n tells you which running times can finish in about a second:
| Largest n | Fits in ~1 s | Too slow |
|---|---|---|
| n ≤ 20 | O(2n) — try every subset | O(n!) once n passes about 11 |
| n ≤ 3 000 | O(n²) = 9·106 | O(n³) = 2.7·1010 |
| n ≤ 105 to 106 | O(n log n) or O(n) | O(n²) = 1010 or more |
| n ≤ 108 | O(n) with a tiny constant | O(n log n) starts to hurt |
When sorting helps
Sorting puts equal values next to each other and puts small values on the left and big ones on the right. That alone solves many problems: duplicates become neighbours, anagrams become identical strings, and a sorted list lets two pointers walk inward (the next lesson in this level, Step 13.2 · Two Pointers, is built on that). Sorting costs O(n log n) time but can need only O(1) extra memory if done in place, so it is the right choice when memory is tight or when the problem asks you to keep extra space at O(1). How sorting works is covered in C02 · Insertion & Merge Sort and C07 · Quicksort.
Counting arrays vs maps
Many problems need "how many times does each thing appear?". If the things are small whole numbers you know in advance — the 26 letters a–z, ages 0–150, digits 0–9 — a plain List<int> of counters is fastest: box c − 97 for letter code c (97 is the code of 'a'). If the things can be anything — words, negative numbers, huge numbers, emoji — use a Map. Same idea, different storage:
Two Sum
The task. You get a list of whole numbers nums and one more whole number target. Find two different positions i < j whose values add up to target, and return them as [i, j]. A position may not be used twice, but two positions may hold the same value. If no pair works, return an empty list. (If several pairs work, this version returns the first one it completes while reading left to right.)
nums = [4, 9, 2, 7], target = 9[2, 3]nums = [5, -3, 8, 1], target = 5[1, 2]nums = [6, 6], target = 12[0, 1]- 2 ≤ n ≤ 105
- −109 ≤ nums[i] ≤ 109, duplicates allowed
- −2·109 ≤ target ≤ 2·109
int.Brute force. Try every pair (the panel above): about n²/2 checks, so O(n²) time and O(1) extra space. Fine for n = 1 000 (half a million checks), hopeless for n = 105.
Key insight. For each number x, its partner is fixed: need = target − x. So the inner loop is really asking one question — "have I already passed the value need, and where?" A Map from value to index answers that in one step.
Optimal approach. Walk the list once. For each position i: compute need, look it up in the map; if found, done; otherwise store nums[i] → i and move on. Looking up before storing guarantees a position is never paired with itself.
A second worked example, on the trickiest edge: the same value appears twice and the target is exactly double it.
Complexity. Time O(n) — one pass, and each map lookup or store is O(1) on average. Space O(n) — in the worst case every value goes into the map before the pair is found.
Input size → what is feasible. n ≤ 104: even the brute force (5·107 checks) passes. n = 105 or more: only the one-pass map version fits in a second.
nums[i] in the map before looking up need: with nums = [5, 1, 7] and target 10 you would "find" 5 + 5 using index 0 twice. (2) Returning the values instead of the positions. (3) Sorting the list first and then returning positions from the sorted copy — the indices no longer match the original list. (4) Using seen.containsKey(need) and then seen[need]!: correct, but two lookups; reading seen[need] once and testing for null does one.Follow-ups interviewers ask.
- The list is already sorted. Then you need no map at all: put one pointer at each end and move them inward (sum too small → move the left one right; too big → move the right one left). O(n) time and O(1) space. This two-pointer idea is the subject of Step 13.2 · Two Pointers, the next lesson in this level; the code is in the question bank below.
- Return every pair of values, not just one. Count each value first, then pair x with target − x, taking care with x + x. See the bank.
- Three numbers that sum to zero. Sort, fix one number, then run the sorted two-pointer search on the rest: O(n²).
The idea underneath: a hash map gives O(1) average lookup — C11 · Hash Tables builds one from scratch, and D27 · Set & Map methods lists the Dart calls used here.
Contains Duplicate
The task. Given a list of whole numbers, answer true if some value appears at least twice, and false if every value is different.
nums = [3, 1, 4, 1]truenums = [10, 20, 30]falsenums = [-5]falsefalse.- 0 ≤ n ≤ 105
- −109 ≤ nums[i] ≤ 109
Brute force. Compare every pair: O(n²) time, O(1) space.
Key insight. The inner loop asks "have I seen this value before?". A Set remembers everything seen so far and answers in O(1) on average.
Optimal approach. One pass; for each x, if it is already in the set, answer true immediately; otherwise add it. In Dart, Set.add returns false when the value was already there, so the check and the insert are one call.
The sorting alternative. After sorting, equal values sit side by side, so one scan comparing neighbours finds a repeat. O(n log n) time; O(1) extra space if you are allowed to sort the caller's list in place (this version copies it to be polite, which costs O(n)).
Complexity. Set version: time O(n) on average (one pass, O(1) per add), space O(n) for the set. Sorting version: time O(n log n), extra space O(1) in place.
Input size → what is feasible. n ≤ 103: anything works. n = 105 to 107: Set or sort. Memory-limited (say 108 numbers, no room for a second copy): sort in place.
nums.toSet().length != nums.length — correct and short, but it always builds the whole set even when the very first two numbers repeat; the loop can stop early. (2) Sorting the caller's list without being asked: the caller's data changes order behind their back. (3) Forgetting the empty list: the answer is false, and your code must not read nums[0].Follow-ups interviewers ask. Duplicates only count when they are at most k positions apart (keep the latest index of each value in a Map). Values within t of each other and at most k apart (bucket the values by width t + 1). Return the first repeated value. All three are in the bank below.
Underlying ideas: C11 · Hash Tables (why add is O(1) on average) and C02 · Insertion & Merge Sort (the sorting alternative).
Valid Anagram
The task. Two words s and t made of lowercase letters a–z. Answer true if t uses exactly the same letters as s, each the same number of times, just in a different order (an anagram); otherwise false.
s = "night", t = "thing"trues = "apple", t = "paper"falses = "ab", t = "abc"false- 1 ≤ |s|, |t| ≤ 5·104
- only lowercase English letters a–z
Brute force. For each letter of s, search t for an unused copy and cross it out: O(n²). Or sort both words and compare: O(n log n) — a fine answer, but not the best.
Key insight. Order does not matter, only how many of each letter. With only 26 possible letters, a List of 26 counters is a perfect "map" whose key is letter code − 97.
Optimal approach. If the lengths differ, answer false. Otherwise walk both words together: the letter from s adds 1 to its box, the letter from t takes 1 away. At the end every box must be 0.
Complexity. Time O(n) — one pass over both words plus 26 checks. Space O(1) — always exactly 26 counters, however long the words are.
Input size → what is feasible. Words up to 106 letters: counting is instant. The sort-and-compare version also passes up to about 106, just with more work.
t.codeUnitAt(i) reads past the end of a shorter t, or "ab" vs "abb" slips through on a one-sided count. (2) Using two Sets instead of counts: {"a","b"} equals {"a","b"} for "aab" and "abb", which are not anagrams. (3) Assuming only a–z when the input can contain capitals, spaces or accented letters: then use a Map keyed by runes (Unicode code points) — see the bank, and string internals in D24 · String methods.Follow-ups interviewers ask. Support any Unicode text (Map over runes). Find all anagrams of a short word inside a long text (slide a window of letter counts along the text). Group many words by anagram — the next problem.
Underlying idea: counting instead of comparing, the same trick as counting sort in C08 · Counting, Radix & Bucket Sort.
Group Anagrams
The task. Given a list of lowercase words, put words that are anagrams of each other into the same group. Return the groups. (This version lists groups in the order their first word appears, and keeps words inside a group in input order.)
words = ["stop", "pots", "bat", "tops", "tab", "cat"][["stop", "pots", "tops"], ["bat", "tab"], ["cat"]]words = [""][[""]]words = ["ab", "ba", "abc"][["ab", "ba"], ["abc"]]- 1 ≤ number of words n ≤ 104
- 0 ≤ word length L ≤ 100
- lowercase a–z
opst. Every book with the same label goes on the same shelf. One look at each book, one shelf per label.Brute force. Compare each word with every other word using the Valid Anagram check: O(n² · L).
Key insight. Give every word a key (a canonical form) that is identical for anagrams and different otherwise. Sorting the letters gives such a key. Then a Map from key to list of words does the grouping in one pass.
Optimal approach. For each word, compute its key by sorting its letters, and append the word to that key's list (creating the list the first time). Dart's putIfAbsent does "create if missing, then give it to me" in one call. A Dart Map literal remembers insertion order, so the groups come out in order of first appearance.
Complexity. Time O(n · L log L) — each of the n words is sorted once (L letters). Space O(n · L) — the keys and the groups hold every letter once more.
Input size → what is feasible. n = 104 words of length 100: sorting keys is about 7·106 steps (instant). Very long words (L = 104): switch to a counting key (26 counts), which is O(L) per word — in the bank.
List as a Map key in Dart: two different lists with equal contents are different keys, because List uses identity for ==. Turn the key into a String.Follow-ups interviewers ask. Use a counting key so each word costs O(L) instead of O(L log L). Return only the largest group. Handle uppercase and spaces ("Dormitory" vs "dirty room") by normalising first.
Underlying ideas: sorting letters (C02 · Insertion & Merge Sort) and Map with list values (D27 · Set & Map methods, see putIfAbsent).
Top K Frequent Elements
The task. Given a list of whole numbers and a number k, return the k values that appear most often, most frequent first. You may assume the answer is unique (no tie at the cut-off). (When frequencies tie inside the answer, this version lists the value that appeared first in the input first.)
nums = [5, 5, 5, 2, 2, 9], k = 2[5, 2]nums = [8], k = 1[8]nums = [4, -1, 4, -1, 4, 6], k = 2[4, -1]- 1 ≤ n ≤ 105
- −104 ≤ nums[i] ≤ 104
- 1 ≤ k ≤ number of distinct values
- the answer is unique
Brute force. Count with a Map, then sort the distinct values by count: O(n log n). Correct, and a good first answer.
Key insight. Counts are whole numbers between 1 and n. Numbers in a small known range can be "sorted" by dropping them into buckets — the idea behind bucket sort — in O(n).
Optimal approach. (1) Count each value. (2) Make n + 1 buckets; put each value into the bucket numbered by its count. (3) Walk the buckets from n down to 1, collecting values until you have k.
Complexity. Time O(n) — counting is n steps, filling buckets is one step per distinct value, and the bucket walk visits n + 1 buckets. Space O(n) for the counts and the buckets.
Input size → what is feasible. n ≤ 105: buckets or sorting both pass. n = 107 in a stream with small k: a size-k min-heap uses only O(k) memory besides the counts — in the bank, and heaps are explained in C06 · Heapsort & Priority Queues.
max count buckets but indexing by count n when every value is the same — off by one; n + 1 buckets (0 to n) is always safe. (2) Returning the counts instead of the values. (3) Using a max-heap of all distinct values: O(n log n) and O(n) heap memory — no better than sorting. The useful heap is a min-heap capped at size k, which throws away the weakest candidate each time.Follow-ups interviewers ask. Use a size-k heap (better when k is tiny and the data is huge or streamed). Top k frequent words, with ties broken alphabetically. Top k when the counts change over time. All with code in the bank.
Underlying ideas: bucket sort in C08 · Counting, Radix & Bucket Sort; heaps in C06 · Heapsort & Priority Queues.
Product of Array Except Self
The task. Given a list nums, build a new list answer where answer[i] is the product of every number except nums[i]. You may not use division, and the work must be O(n).
nums = [2, 3, 4, 5][60, 40, 30, 24]nums = [-1, 2, 0, 4][0, 0, -8, 0]nums = [0, 5, 0][0, 0, 0]- 2 ≤ n ≤ 105
- −30 ≤ nums[i] ≤ 30
- every prefix and suffix product fits in a 32-bit int
- division is not allowed
Brute force. For each i, multiply all the others: O(n²). Using the total product and dividing by nums[i] would be O(n), but division is banned — and it breaks on zeros anyway.
Key insight. "Everything except i" = "everything left of i" × "everything right of i". Both are running products: a prefix product built left to right and a suffix product built right to left.
Optimal approach. First pass: write the prefix product (everything left of i) into answer[i]. Second pass, right to left: multiply answer[i] by the suffix product (everything right of i). Only two extra variables are used, so the extra space is O(1) besides the output.
Complexity. Time O(n) — two passes. Extra space O(1) — just prefix and suffix; the output list is required by the task and is not counted.
Input size → what is feasible. n = 105 or 106: two passes, instant. The brute force passes only up to about n = 104 (108 multiplications).
prefix before writing it into answer[i]: then nums[i] sneaks into its own product. (2) Starting prefix at 0 instead of 1 — the product of "nothing" is 1, just as the sum of nothing is 0. (3) Ignoring overflow: in Dart native code int is 64-bit and silently wraps around past about 9.2·1018; on the web, numbers are JavaScript doubles and lose precision past 253. The constraints here keep every product small.Follow-ups interviewers ask. Division is allowed: count the zeros (none: total ÷ nums[i]; one zero: only the zero's position is non-zero; two or more: all zeros). Answer the same question modulo a prime. Product of every range [l, r] — prefix products again. See the bank.
Underlying idea: prefix sums and products, the same running-total trick used for "subarray sum equals k" in the bank. List basics are in D26 · List methods.
Longest Consecutive Sequence
The task. Given an unsorted list of whole numbers, find the length of the longest run of consecutive values (like 7, 8, 9, 10) that all appear somewhere in the list, in any order. Repeated values count once. The work must be O(n).
nums = [8, 3, 10, 2, 9, 4, 7]4nums = [5, 5, 6, 5]2nums = []0- 0 ≤ n ≤ 105
- −109 ≤ nums[i] ≤ 109, duplicates allowed
Brute force. For every number x, count upward x + 1, x + 2, … by scanning the list each time: O(n³) with list scans, O(n²) with a set but no "start" check. Sorting and scanning for runs is O(n log n) — a decent answer, but not the O(n) the task demands.
Key insight. Put every value in a Set for O(1) "is v present?". Then only begin counting at values x where x − 1 is not present. Each run is then walked exactly once, from its first value.
Optimal approach. Build the set. For each value x in the set: if x − 1 is in the set, skip x (it is in the middle of some run). Otherwise count x, x + 1, x + 2, … while they are present, and keep the best length.
Complexity. Time O(n) on average. The inner while looks scary inside a for, but it only runs from run starts, and the runs do not overlap — so across the whole program the while body runs at most n times in total. Space O(n) for the set.
Input size → what is feasible. n = 105 to 106: the set version is about 2n to 3n lookups. Without the "start only" check, a single long run of length n makes the work n²/2 — 5·109 steps for n = 105.
x − 1 check: the code still gives the right answer, but on input 1, 2, …, n it does n²/2 steps. (2) Looping over nums instead of the set: with many duplicates of a run start, the same run is walked many times. (3) Returning 1 for an empty list: the answer is 0.Follow-ups interviewers ask. Return the run itself, not only its length (remember the best start). Numbers arrive one at a time and you must report the longest run after each arrival — keep run lengths at the two ends of each run in a Map (in the bank). Do it with union-find (merge x with x + 1).
Underlying idea: O(1) average membership tests from C11 · Hash Tables; Set methods in D27 · Set & Map methods.
Quiz
Interview questions
Variations and follow-ups of the seven problems above — the questions interviewers move to once you have solved the classic version. Every solution is tested in Dart on its examples and on hundreds of random inputs against a slow reference.
Cheat sheet
| Problem | Pattern | Time | Space | Key trick |
|---|---|---|---|---|
| Two Sum | Map: value → index | O(n) | O(n) | Look up target − x before storing x |
| Contains Duplicate | Set membership | O(n) | O(n) | Set.add returns false on a repeat; sort for O(1) space |
| Valid Anagram | Counting array | O(n) | O(1) | s adds, t subtracts, all 26 boxes must end at 0 |
| Group Anagrams | Map: canonical key → list | O(n · L log L) | O(n · L) | Key = sorted letters (or a comma-separated letter count) |
| Top K Frequent | Count + bucket by frequency | O(n) | O(n) | Counts are 1..n, so bucket instead of sorting; size-k min-heap for streams |
| Product Except Self | Prefix × suffix products | O(n) | O(1) extra | Write prefix first, then multiply by suffix on the way back |
| Longest Consecutive | Set + start-of-run check | O(n) | O(n) | Only count from x when x − 1 is missing |
All times are average-case for hash-based structures; a hash table's worst case is O(n) per operation if every key collides (see question p01-q31 in the bank).