Arrays & Hashing
Twelve drills that teach the single most useful trade in interview coding: spend O(n) memory on a hash map, set, or counting array so that every "have I seen this before?" question costs O(1) instead of a nested loop. Each drill has the reasoning, a traced diagram, a compilable Java solution, and the bugs people actually write.
Trade Memory for Lookups
An array gives you O(1) access by position. A hash table gives you O(1) access by value: it turns a key into an array index with a hash function, so "is 7 in here?" or "how many times have I seen 'a'?" no longer needs a scan. Almost every drill on this page is a nested-loop brute force (O(nΒ²)) that collapses to a single pass (O(n)) the moment you remember the right fact about the elements you already walked past.
Java gives you three tools for this. HashSet<Integer> answers membership ("seen it?"). HashMap<K,V> remembers something about each value β its index, its count, its group. And when keys are tiny and dense (lowercase letters, digits), a plain int[26] is a perfect hash table with no hashing, no boxing, and guaranteed O(1).
hash(coat) & 7, and later the attendant walks straight to hook 7 instead of searching every hook. Two coats on the same hook (a collision) just means checking two coats on that hook β still nowhere near checking all of them.
- Average-case O(1) for
get,put,contains; Java 8+ turns long bucket chains into balanced trees, so the worst case is O(log n), not O(n). - Boxing matters:
HashMap<Integer,Integer>allocates objects. When keys are bounded (letters, small ints) anint[]is 5β10Γ faster. - Never rely on
HashMap/HashSetiteration order β it depends on hash codes and capacity, not insertion order. UseLinkedHashMapif order matters.
How to Think About These Problems
Before writing code, classify the question. The diagram below is the order in which to ask β the first "yes" usually names the template from Chapter 03.
Four Reusable Templates
Learn these four shapes and the twelve drills become variations. Each template is a single left-to-right pass; what changes is what you remember about the prefix you already walked.
int[26] for lowercase letters, HashMap for anything else.Two Sum
Given an integer array nums and an integer target, return the indices of the two numbers that add up to target. Exactly one answer exists and you may not use the same element twice.
target β x. A specific-value search over earlier elements is a hash lookup.
Brute force β optimal. Checking every pair (i, j) is O(nΒ²). Sorting + two pointers is O(n log n) but loses the original indices unless you sort index pairs. A single pass with a HashMap<value, index> does it in O(n): at index i, every earlier element is already in the map, so if a partner exists to the left you find it immediately; if the partner is to the right, it will find you when its turn comes.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Nested loops | O(nΒ²) | O(1) | Every pair |
| Sort + two pointers | O(n log n) | O(n) | Must carry original indices |
| One-pass HashMap β optimal | O(n) | O(n) | One lookup + one insert per element |
- Inserting
nums[i]before looking up its complement: with target 12 and a single 6, the code returns[2, 2]β using one element twice. - Duplicates such as
[3, 3], target 6, still work with lookup-then-insert: index 1 finds index 0. Overwriting the map entry for repeated values is harmless. - Returning values instead of indices, or 1-based indices (that is Two Sum II, Drill 18).
- Returning an empty array when nothing is found hides bugs; the problem guarantees an answer, so throwing is clearer.
Contains Duplicate Β· Valid Anagram
Given an integer array nums, return true if any value appears at least twice, and false if every element is distinct.
Set.add() already tells you the answer: it returns false when the element was present. The membership test and the insert are one call.
Brute force β optimal. Comparing all pairs is O(nΒ²). Sorting and checking neighbours is O(n log n) time and O(1) extra space (if you may mutate the input) β a fine answer when memory is tight. A HashSet gives O(n) time and stops at the first repeat.
| Step | num | set before | add() returns | Action |
|---|---|---|---|---|
| 1 | 1 | { } | true | continue |
| 2 | 2 | {1} | true | continue |
| 3 | 3 | {1, 2} | true | continue |
| 4 | 1 | {1, 2, 3} | false | return true |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Nested loops | O(nΒ²) | O(1) | |
| Sort + scan neighbours | O(n log n) | O(1)* | *mutates input; Arrays.sort on int[] is in-place |
| HashSet β optimal | O(n) | O(n) | Stops at first duplicate |
- Writing
if (set.contains(x)) return true; set.add(x);works, but hashes twice βadd()'s boolean does both. - Comparing boxed
Integerwith==instead ofequals(only safe in the cache range β128..127) β another reason to let the set compare. - Pre-size the set (
new HashSet<>(2 * n)) for large inputs to avoid repeated rehashing.
Given two strings s and t of lowercase English letters, return true if t is an anagram of s β the same letters with the same multiplicities, in any order.
s increment and t decrement β equal multisets cancel to all zeros.
Brute force β optimal. Sorting both strings and comparing is O(n log n) and simple. Counting is O(n) with a fixed 26-slot array β O(1) extra space because the alphabet does not grow with n. A length check first rejects most non-anagrams for free.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Sort both, compare | O(n log n) | O(n) | toCharArray copies |
| int[26] counts β optimal | O(n) | O(1) | 26 slots regardless of n |
- Skipping the length check: then
"ab"vs"a"throwsStringIndexOutOfBoundsExceptionin the shared loop. c - 'a'assumes lowercase ASCII. For Unicode input useHashMap<Integer,Integer>overs.codePoints().- Comparing
Arrays.sorted char arrays with==(reference equality) instead ofArrays.equals.
First Unique Character Β· Intersection
Given a string s of lowercase letters, return the index of the first character that occurs exactly once. If there is none, return -1.
Brute force β optimal. For each index, scan the rest for a repeat: O(nΒ²). Two passes with an int[26] are O(n). A LinkedHashMap also works (it remembers insertion order) but is slower and allocates.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Check each char against all others | O(nΒ²) | O(1) | |
| Two passes, int[26] β optimal | O(n) | O(1) | 26 counters |
- Scanning the count array in alphabet order in pass 2 β that returns the alphabetically first unique letter, not the first by position.
- Returning the character instead of its index.
- Empty string: both loops skip and
-1is returned β correct, but test it.
Given two integer arrays, return their intersection: every value that appears in both, each value listed once, in any order.
Brute force β optimal. Nested loops plus a "already added?" check is O(nΒ·m). Sorting both and walking with two pointers is O((n + m) log) and O(1) extra β handy if the inputs are already sorted. Hash sets give O(n + m).
| nums2[j] | in set(nums1) = {4, 9, 5}? | result set after |
|---|---|---|
| 9 | yes | {9} |
| 4 | yes | {9, 4} |
| 9 | yes (already in result) | {9, 4} |
| 8 | no | {9, 4} |
| 4 | yes (already in result) | {9, 4} |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Nested loops + contains check | O(nΒ·m) | O(k) | |
| Sort both + two pointers | O(n log n + m log m) | O(1) | extra beyond output |
| Two HashSets β optimal | O(n + m) | O(n) |
- Using a
Listfor the result: duplicates in nums2 (the two 4s) appear twice. - Printing
HashSetcontents and asserting a specific order in tests β iteration order is unspecified; sort before comparing. - Confusing this with Intersection II (LC 350), which keeps multiplicities β that one needs a count map, not a set.
Group Anagrams
Given an array of lowercase strings, group the anagrams together. Return the groups in any order.
HashMap<key, List>.
Brute force β optimal. Comparing every pair of words with Valid Anagram is O(nΒ²Β·k). Sorting each word as the key gives O(nΒ·k log k). Building a count-signature key is O(nΒ·k) β linear in the total input size. Both are accepted; the sorted key is shorter to write in an interview.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Pairwise isAnagram | O(nΒ²Β·k) | O(nΒ·k) | |
| Sorted-string key | O(nΒ·k log k) | O(nΒ·k) | simplest to write |
| Count-signature key β optimal | O(nΒ·k) | O(nΒ·k) | k = max word length |
- Concatenating counts without a separator: counts (1, 11) and (11, 1) both become
"111". The#delimiter makes the key unambiguous. - Using
chars.toString()as the key β that is the array's identity hash ([C@1b6d3586), never equal for two arrays. Usenew String(chars). - Expecting the output in input order:
HashMap.values()order is arbitrary. UseLinkedHashMapif a stable order is required. - Empty string
""is a valid word; it gets the all-zero key and forms its own group.
Product of Array Except Self
Given nums, return answer where answer[i] is the product of every element except nums[i]. Do it in O(n) without the division operator.
Brute force β optimal. Multiplying all others for each i is O(nΒ²). "Total product Γ· nums[i]" is O(n) but forbidden β and breaks on zeros. Two prefix-product passes are O(n); writing the left products directly into the output array and folding the right products in with a single variable makes the extra space O(1) (the output array does not count).
| Approach | Time | Space | Notes |
|---|---|---|---|
| Nested loops | O(nΒ²) | O(1) | |
| Separate left[] and right[] arrays | O(n) | O(n) | clearest first version |
| Output array + running right product β optimal | O(n) | O(1) extra | output not counted |
- Using division: fails with a zero (divide by zero) and is disallowed by the problem anyway.
- Updating
right *= nums[i]beforeanswer[i] *= rightβ then answer[i] includes nums[i] itself. - Overflow: the problem guarantees products fit in 32 bits; in general code use
long. - Single element
[5]: the empty product is 1, so the answer is[1].
Subarray Sum Equals K
Given an integer array nums (values may be negative) and an integer k, return the number of non-empty contiguous subarrays whose sum equals k.
P[j+1] β k. So it is Two Sum on prefix sums: "how many earlier prefixes are my complement?"
Brute force β optimal. All O(nΒ²) subarrays with a running sum is O(nΒ²). Sliding window does not work: with negative numbers, growing the window can shrink the sum, so there is no monotonic rule for moving the left edge. A hash map from prefix sum β how many times seen gives O(n).
| Approach | Time | Space | Notes |
|---|---|---|---|
| All subarrays (running sum) | O(nΒ²) | O(1) | |
| Prefix sum + HashMap β optimal | O(n) | O(n) | counts, not just presence |
- Forgetting
map.put(0, 1): subarrays that start at index 0 (like[1, 2]above) are never counted. - Inserting the current prefix before the lookup: with
k = 0every position falsely matches itself. - Storing booleans/a set instead of counts β several earlier prefixes can share the same value (zeros, negatives) and each one is a different subarray.
- Reaching for sliding window: correct only when all numbers are positive.
Majority Element Β· Single Number
Given an array of size n, return the element that appears more than βn / 2β times. The majority element is guaranteed to exist.
Brute force β optimal. Counting each value with a nested loop is O(nΒ²). A HashMap count is O(n) time and O(n) space. Sorting and returning nums[n/2] is O(n log n). BoyerβMoore voting keeps two integers: O(n) time, O(1) space.
| i | x | candidate | count | why |
|---|---|---|---|---|
| 0 | 2 | 2 | 1 | count was 0 β new candidate 2 |
| 1 | 2 | 2 | 2 | vote for |
| 2 | 1 | 2 | 1 | cancel one vote |
| 3 | 1 | 2 | 0 | cancel one vote |
| 4 | 1 | 1 | 1 | count was 0 β new candidate 1 |
| 5 | 2 | 1 | 0 | cancel one vote |
| 6 | 2 | 2 | 1 | count was 0 β new candidate 2 |
| Approach | Time | Space | Notes |
|---|---|---|---|
| Count each with nested loop | O(nΒ²) | O(1) | |
| HashMap counts | O(n) | O(n) | |
| Sort, take middle | O(n log n) | O(1) | mutates input |
| BoyerβMoore β optimal | O(n) | O(1) | needs the majority guarantee |
- Using BoyerβMoore when a majority is not guaranteed: the survivor is only a candidate; you must re-count it in a second pass.
- Resetting the candidate when count hits zero but forgetting to count the current element for it β the check must happen before the vote, as in the code.
- Confusing ">n/2" (majority) with "most frequent":
[1,1,2,2,3]has no majority and the vote returns 3.
Every element of a non-empty integer array appears exactly twice except one. Find that single element in O(n) time and O(1) extra space.
Brute force β optimal. A HashSet that adds and removes (or a count map) is O(n) time but O(n) space. 2Β·sum(set) β sum(nums) is cute but also O(n) space and can overflow. XOR-ing everything is O(n) time and one int of space.
| Approach | Time | Space | Notes |
|---|---|---|---|
| HashSet add/remove | O(n) | O(n) | |
| XOR fold β optimal | O(n) | O(1) | works for negatives too (two's complement) |
- Starting the accumulator at
nums[0]and looping from 0 β XORs the first element twice and returns the wrong answer. - Applying XOR when duplicates appear three times (LC 137) β that variant needs per-bit counting mod 3.
- Thinking XOR breaks on negatives: it operates on all 32 bits, so
[-3, 7, 7]returns β3.
Best Time to Buy and Sell Stock I & II
prices[i] is a stock's price on day i. Choose one day to buy and a later day to sell to maximise profit. Return the maximum profit, or 0 if no profit is possible.
Brute force β optimal. Every (buy, sell) pair with buy < sell is O(nΒ²). Template D collapses it: the state is (minPrice, best), updated in O(1) per day, for O(n) time and O(1) space. (Equivalently: Kadane's algorithm on the day-to-day differences.)
| Approach | Time | Space | Notes |
|---|---|---|---|
| All buy/sell pairs | O(nΒ²) | O(1) | |
| Min so far β optimal | O(n) | O(1) | one pass |
- Taking
max(prices) β min(prices): in[7, 1, 5, 3, 6, 4]that happens to work, but in[3, 8, 1]the max comes before the min β answer 5, not 7. - Initialising
besttoInteger.MIN_VALUEβ a falling market must return 0, not a negative profit. - Updating
minPriceand profit in the wrong order is harmless here (profit 0 on the same day), but in variants with fees it matters β be deliberate.
Same prices, but now you may complete as many transactions as you like β holding at most one share at a time (sell before you buy again). Return the maximum total profit.
Brute force β optimal. Trying every set of non-overlapping transactions is exponential; a two-state DP (holding / not holding) is O(n) and generalises to fees and cooldowns. For the plain version the greedy sum of positive differences is O(n), O(1), and provably optimal because a long trade can always be split into consecutive daily trades with the same total.
| Approach | Time | Space | Notes |
|---|---|---|---|
| Enumerate transaction sets | O(2βΏ) | O(n) | |
| DP hold / free | O(n) | O(1) | extends to fee / cooldown |
| Greedy sum of gains β optimal | O(n) | O(1) |
- Reusing Drill 11's single-trade logic β it returns 5, not 7.
- Trying to find explicit valley/peak pairs with nested while-loops: correct but easy to get off-by-one at the array end. The difference sum has no edge cases.
- The greedy shortcut stops working once a per-transaction fee exists (LC 714) β switch to the DP.
Summary & What to Practice Next
| Drill | Problem | Pattern | Time | Space | Difficulty |
|---|---|---|---|---|---|
| 01 | Two Sum | complement map | O(n) | O(n) | Easy |
| 02 | Contains Duplicate | hash set | O(n) | O(n) | Easy |
| 03 | Valid Anagram | frequency count | O(n) | O(1) | Easy |
| 04 | First Unique Character | frequency count | O(n) | O(1) | Easy |
| 05 | Intersection of Two Arrays | hash set | O(n + m) | O(n) | Easy |
| 06 | Group Anagrams | canonical key | O(nΒ·k) | O(nΒ·k) | Medium |
| 07 | Product of Array Except Self | prefix / suffix | O(n) | O(1)* | Medium |
| 08 | Subarray Sum Equals K | prefix sum + map | O(n) | O(n) | Medium |
| 09 | Majority Element | boyer-moore | O(n) | O(1) | Easy |
| 10 | Single Number | xor fold | O(n) | O(1) | Easy |
| 11 | Best Time to Buy/Sell Stock | min so far | O(n) | O(1) | Easy |
| 12 | Best Time to Buy/Sell Stock II | greedy gains | O(n) | O(1) | Medium |
* O(1) extra space β the output array is not counted.
- Inner loop searches for one value? Replace it with a
HashMap/HashSetlookup (Drills 01, 02, 05). - Order does not matter? Count.
int[26]for letters; a count signature doubles as a grouping key (Drills 03, 04, 06). - Contiguous range? Prefix sums/products; add a map of earlier prefixes to count ranges (Drills 07, 08).
- O(1) space? Find the running state: min-so-far, a vote, an XOR (Drills 09β12).
- Lookup before insert β the single most common bug on this page (Drills 01, 08).
- Continue to Coding Drills 02 Β· Two Pointers β the O(1)-space alternative when the input is sorted.
- Refresh the data structures: HashMap, HashSet, Array.
- More problems in the same family: LeetCode Β· Arrays & Hashing, LeetCode Β· Hash Map, LeetCode Β· Math & Bits.
- Why the stock drills are greedy: Greedy algorithms; how to state the costs: Complexity.