DSA · Foundations · Toolkit
Dynamic Arrays, Maps and Sets
Every language ships the same small box of containers. This lesson is about using them — what each one is for, what it costs, and why one hash map turns a nested loop into a single pass.
Count the letters in BANANA →01 The idea
Stop writing the second loop
Whatever language you answer an interview in, you get the same handful of containers. Java calls them ArrayList, HashMap, HashSet, ArrayDeque, PriorityQueue. C++ calls them vector, unordered_map, unordered_set, deque, priority_queue. Python calls them list, dict, set, deque, heapq. Different names, the same five ideas.
How they are built inside is a later module. What you need first is what each one is for and what it costs, because the gap between a passing answer and a good one is usually one line. Instead of walking the array a second time to find out how many times a value appeared, you look the value up.
02 Hand trace
Counting the letters in BANANA
Six letters: B A N A N A. You read them left to right and keep one map from letter to count. Every letter triggers the same two moves — look the key up, then either put it in at 1 or add 1 to what is already there. Follow A: it goes in at letter 2 and is bumped twice after that. The columns below are the map's keys, in the order they were created.
Six letters in, six map updates out — one per letter, and the string is never read a second time. The counts add back to 6, which is the check worth doing every single time. Reading off the winner then costs a walk over 3 keys, not 6 letters. Section 03 is this trace written as code, twice.
03 Two ways to count
26 slots, or a hash map
Both versions count the letters of B A N A N A and both report A with 3. The left one uses the letter itself as an array index. The right one hashes the letter. On this input the difference is not speed — it is what happens when the keys stop being letters.
Fixed array · one slot per letter
count = new int[26] # all zerofor ch in s: count[ch - 'A'] += 1 # 'B'-'A' is 1best = 0for i = 1 to 25: if count[i] > count[best]: best = ireturn letter(best) # 'A', seen 3
Hash map · any key at all
freq = empty mapfor each item in list: freq[item] = freq.getOrDefault(item, 0) + 1best = nonefor (key, count) in freq: if best == none or count > freq[best]: best = keyreturn best # 'A', seen 3
These are not two algorithms. Both do exactly one update per item. The array finishes in O(n) flat; the map finishes in O(n) on average, because each of its n updates is only constant-time on average. The array is a hash map whose hash function is ch - 'A' — a hash so perfect that two different letters can never collide, which is why it needs no buckets and no worst case.
What the fixed array buys. Every update on line 3 is a single memory write into a block of 26 integers. There is no hashing to compute, no entry object to allocate, and the whole table fits in a cache line or two. When you are told the input is lowercase letters only, this is the answer an interviewer is fishing for.
What it costs. It only works when you can list every possible key in advance and that list is short — 26 letters, 10 digits, 256 ASCII values. The moment the keys are words, user IDs or arbitrary integers, 26 slots becomes four billion, and only the map survives.
Why the map's second loop is shorter. Line 5 on the left walks all 26 slots even though only 3 were ever touched, so it makes 25 comparisons. Line 5 on the right walks the 3 keys that actually exist and makes 2. The array pays for the alphabet; the map pays only for what turned up.
One place they disagree. Both use a strict >, so on a tie neither swaps — but "first" means different things. The array keeps the lowest index, so it reports the alphabetically earliest of the tied letters. The map keeps the earliest key it inserted, so it reports whichever tied letter showed up first in the input. On WWQQ the array says Q and the map says W. When a question has ties, say out loud which winner you are returning.
05 Cheat sheet
The toolkit, and what each piece costs
| Container or operation | Reach for it when | Cost |
|---|---|---|
| Dynamic array — ArrayList, vector, list | You need indexed access and do not know the size up front | get O(1) · append O(1) amortised |
| Insert or delete in the middle of one | Almost never on purpose — everything after it shifts | O(n) |
| String builder — StringBuilder, "".join(parts), C++ string::+= | You are assembling a string inside a loop | O(n) for n pieces |
| + or += on a Java or Python string inside a loop | Never — this is the trap, not the tool. The string is immutable, so every step copies it whole | O(n²) |
| Hash map — HashMap, unordered_map, dict | Count, group, or look something up by key | put / get / contains O(1) average |
| Hash map when the keys collide badly | The worst case an interviewer will ask about | O(n) per operation |
| Hash set — HashSet, unordered_set, set | The only question is "have I seen this before?" | add / contains O(1) average |
| Built-in sort with a comparator | You need an order the default does not give you | O(n log n) |
| Stack, queue, deque — all ArrayDeque / deque | Last-in-first-out, first-in-first-out, or both ends | push / pop / peek O(1) |
| Priority queue (heap) — preview | You repeatedly need the smallest or largest item | push / pop O(log n) · peek O(1) |
06 Where & why
When the map is the right answer, and when it is lazy
A hash map is the default answer to most counting and lookup questions and it is rarely wrong. It is just not always the cheapest thing available, and knowing where a plain array or a sorted scan beats it is what separates a memorised answer from an understood one.
Reach for it when…
Use something else when…
| Situation | Better choice | Why |
|---|---|---|
| The keys are exactly the 26 letters, or the 10 digits | int[26] or int[10] | The index is the hash, so there is nothing to compute and nothing to allocate. No collisions means no worst case at all. |
| You need the keys back in sorted order | TreeMap, std::map, or sorting the key list | No hash map gives you sorted order — Java's is arbitrary, Python's is insertion order. A tree map keeps keys sorted, at O(log n) per operation instead of O(1). |
| You only ever want the smallest or largest, over and over | Priority queue | A map cannot hand you the minimum without scanning every key it holds. A heap peeks in O(1) and removes in O(log n). |
| The data is already sorted and you are hunting duplicates | A neighbour scan | Equal values sit next to each other, so comparing each item with the one before it answers it in O(n) time and O(1) extra space. The set would buy nothing. |
07 Interview questions
Answer these out loud
Each answer is about twenty seconds spoken. Give the direct answer first, then the reason, then the cost.
What is a dynamic array, and what does it cost to add to one?
Appending is O(1), but the memory underneath has a fixed size. What happens when it fills up?
Why is building a string with + inside a loop a problem?
What is a hash map, and why do we say its operations are O(1) "on average"?
HashMap or HashSet — how do you choose?
Count how many times each letter appears in BANANA. What do you write, and what does it cost?
For letters you could use an int[26] instead of a map. Which do you pick?
How do you check whether an array contains a duplicate?
You need to sort by something other than the natural order. How?
Stack, queue and deque — what is the difference, and what do you actually instantiate?
What is a priority queue for, and how is it different from keeping a sorted list?
When would a hash map actually be the wrong tool?
08 Practice problems
Six problems, one map
Every one of these is the BANANA pass with something different recorded in the value — a count, an index, a signature, a list. Solve them in order; the twist is the part worth thinking about.