Java Collections for Coding Interviews: Heaps, Deques, TreeMap
Java idioms behind common interview problems: frequency maps, top-k heaps, monotonic stacks, sliding windows, prefix sums, TreeMap, BFS and bitmasks.
- Course: Java study plan
- Module: Interview idioms
- Kind: Lesson
- Reading time: 15 min
- Runtime: OpenJDK 18
How do you find the top k frequent elements in Java?
To find the top k frequent elements in Java, count occurrences with HashMap.merge(key, 1, Integer::sum), then offer the entries to a PriorityQueue ordered by count — a min-heap — and poll whenever its size exceeds k. The heap keeps the k largest in O(n log k) time; poll them out and reverse for descending order, breaking ties in the comparator.
Lesson
Interview problems repeat. Under the surface of "find the k most frequent", "longest substring without repeats", "merge intervals" and "next greater element" are perhaps fifteen shapes, and each has a two-to-six-line Java idiom that experienced candidates type without thinking. This lesson is the sheet: the idiom, the structure it uses, the complexity, and the trap. Learn them as vocabulary — you should be able to say "frequency map, then a min-heap of size k" as one phrase.
Counting and grouping
Map<String, Integer> freq = new HashMap<>();
for (String w : words) freq.merge(w, 1, Integer::sum); // O(n); the frequency map
Map<Integer, List<String>> byLen = new HashMap<>();
for (String w : words) byLen.computeIfAbsent(w.length(), k -> new ArrayList<>()).add(w); // grouping
int[] count = new int[26]; for (char c : s.toCharArray()) count[c - 'a']++; // bounded alphabet: an array beats a map
Trap: boxed == when comparing counts; an alphabet array is both faster and immune.
Top-k and ordering by a key
PriorityQueue<Map.Entry<String, Integer>> heap = // min-heap on count, size k
new PriorityQueue<>(Map.Entry.comparingByValue());
for (var e : freq.entrySet()) { heap.offer(e); if (heap.size() > k) heap.poll(); } // O(n log k)
// heap now holds the k largest; poll to list them ascending, reverse for descending
Use a min-heap to keep the largest k (evict the smallest). For ties, put the secondary key in the comparator (comparingByValue().thenComparing(Map.Entry.comparingByKey(Comparator.reverseOrder())) so that alphabetically earlier words survive). For k close to n, sorting is simpler and just as fast. PriorityQueue has no decreaseKey: for Dijkstra, push duplicates and skip stale entries on poll.
Stacks, queues, deques
Deque<Integer> stack = new ArrayDeque<>(); stack.push(x); stack.pop(); stack.peek();
Deque<Integer> queue = new ArrayDeque<>(); queue.offer(x); queue.poll(); queue.peek();
Never Stack (synchronised, a Vector) or LinkedList (slow, allocation-heavy). ArrayDeque rejects null — use a sentinel or Integer box carefully. Monotonic stack for "next greater element" / histogram problems:
Deque<Integer> idx = new ArrayDeque<>(); // indexes with decreasing values
for (int i = 0; i < n; i++) {
while (!idx.isEmpty() && a[idx.peek()] < a[i]) next[idx.pop()] = i; // a[i] is the next greater for those
idx.push(i);
}
Sliding-window maximum is the same idea with a deque that also drops indexes that left the window from the front — O(n).
Sorted structures: TreeMap navigation
TreeMap<Integer, String> tm = …;
tm.floorKey(x); tm.ceilingKey(x); tm.lowerKey(x); tm.higherKey(x); // nearest keys, O(log n)
tm.firstKey(); tm.lastKey(); tm.headMap(x); tm.tailMap(x, true); tm.subMap(lo, hi);
tm.pollFirstEntry(); // sorted queue with removal by key
Interval scheduling ("is this slot free?"), nearest-value queries, "count of elements ≤ x" (via headMap(x, true).size() — O(n), so use a Fenwick tree if it is hot), and an ordered multiset via TreeMap<Integer, Integer> counts.
Two pointers and sliding windows
int lo = 0; Map<Character, Integer> last = new HashMap<>(); int best = 0;
for (int hi = 0; hi < s.length(); hi++) {
char c = s.charAt(hi);
if (last.containsKey(c) && last.get(c) >= lo) lo = last.get(c) + 1; // shrink past the repeat
last.put(c, hi);
best = Math.max(best, hi - lo + 1); // longest substring without repeats
}
Windows are for contiguous subarrays with a monotone condition; two pointers from both ends for sorted arrays (pair sums). Both O(n). Trap: shrinking with while when the condition is not monotone — check that moving lo right can only help.
Prefix sums and hashing for subarrays
long[] pre = new long[n + 1]; for (int i = 0; i < n; i++) pre[i + 1] = pre[i] + a[i]; // sum(i..j) = pre[j+1] - pre[i]
Map<Long, Integer> seen = new HashMap<>(); seen.put(0L, 1); long run = 0; int count = 0;
for (int x : a) { run += x; count += seen.getOrDefault(run - target, 0); seen.merge(run, 1, Integer::sum); } // subarrays summing to target, O(n)
long prefixes always. 2-D prefix sums for grid rectangles; XOR prefixes for XOR subarrays.
Hash-set tricks
Set<Integer> seen = new HashSet<>();
for (int x : a) { if (seen.contains(target - x)) return true; seen.add(x); } // two-sum, O(n)
new HashSet<>(list).size() == list.size(); // all distinct?
Set<Integer> common = new HashSet<>(aList); common.retainAll(bList); // intersection
Anagram grouping: key = sorted characters (new String(sortedChars)) or a count signature; groupingBy on that key.
Graph skeletons
List<List<Integer>> adj = new ArrayList<>(); for (int i = 0; i < n; i++) adj.add(new ArrayList<>()); // adjacency list
// BFS
Deque<Integer> q = new ArrayDeque<>(); int[] dist = new int[n]; Arrays.fill(dist, -1);
dist[src] = 0; q.offer(src);
while (!q.isEmpty()) { int u = q.poll(); for (int v : adj.get(u)) if (dist[v] < 0) { dist[v] = dist[u] + 1; q.offer(v); } }
// DFS, iterative (no stack overflow)
Deque<Integer> st = new ArrayDeque<>(); boolean[] seen = new boolean[n]; st.push(src);
while (!st.isEmpty()) { int u = st.pop(); if (seen[u]) continue; seen[u] = true; for (int v : adj.get(u)) if (!seen[v]) st.push(v); }
// Dijkstra
PriorityQueue<long[]> pq = new PriorityQueue<>(Comparator.comparingLong(x -> x[0])); // {dist, node}
Grid graphs: int[] dr = {1, -1, 0, 0}, dc = {0, 0, 1, -1} and a bounds check. int[][] for the grid, boolean[][] for visited — not sets of strings.
Memoisation and DP tables
Map<Long, Long> memo = new HashMap<>(); // key: encode (i, j) as i * M + j — cheaper than a String or a List key
long[][] dp = new long[n + 1][m + 1]; Arrays.fill(dp[0], INF); // INF = Long.MAX_VALUE / 4 so INF + INF does not overflow
Prefer arrays to maps for DP over small integer states; encode multi-dimensional keys into one long; choose an INF that survives addition.
Bits
Integer.bitCount(x); Integer.highestOneBit(x); Integer.numberOfTrailingZeros(x);
(mask >> i & 1) == 1; mask | (1 << i); mask & ~(1 << i); mask & (mask - 1); // test, set, clear, drop lowest set bit
for (int sub = mask; sub > 0; sub = (sub - 1) & mask) { … } // enumerate submasks
1 << i is an int — 1L << i for i ≥ 31. >>> for unsigned shifts.
String building and manipulation
new StringBuilder(s).reverse().toString();
sb.setLength(sb.length() - 1); // drop the trailing separator
sb.insert(0, c); sb.deleteCharAt(i); sb.setCharAt(i, c);
String.join(",", list); String.valueOf(chars); new String(chars, from, len);
Character.isLetterOrDigit(c); Character.toLowerCase(c); Character.getNumericValue(c);
The sheet, as sentences
| Problem shape | Say | Structure | Complexity |
|---|---|---|---|
| count things | frequency map / alphabet array | HashMap.merge, int[26] | O(n) |
| k largest / most frequent | min-heap of size k | PriorityQueue | O(n log k) |
| next greater / histogram | monotonic stack | ArrayDeque | O(n) |
| window max/min | monotonic deque | ArrayDeque | O(n) |
| nearest / range queries on sorted keys | tree map navigation | TreeMap.floorKey | O(log n) |
| contiguous subarray with monotone predicate | sliding window | two indexes | O(n) |
| subarray sums | prefix sums + hash map | long[], HashMap | O(n) |
| pair with property | hash set / two pointers | HashSet / sorted array | O(n) / O(n log n) |
| shortest path unweighted / weighted | BFS / Dijkstra | ArrayDeque / PriorityQueue | O(V + E) / O(E log V) |
| overlapping subproblems | memo / DP table | long[][], encoded long keys | states × transitions |
| subsets of ≤ 20 | bitmask | int masks | O(2ⁿ · n) |
Interview angle
- "k most frequent words?" Frequency map, min-heap of size k keyed on count (ties by word), poll and reverse — O(n log k).
- "Longest substring without repeating characters?" Sliding window with last-seen indexes — O(n).
- "Count subarrays summing to k?" Running prefix sum and a map of prefix counts — O(n).
- "Why
ArrayDequeand notStack?"Stackis a synchronisedVectorwith the wrong API;ArrayDequeis faster and idiomatic. - "Deep DFS in Java?" Iterative with an explicit stack to avoid
StackOverflowError.
Key takeaways
- Fifteen shapes; name the shape, then the structure, then the complexity — before writing code.
merge/computeIfAbsentfor counting and grouping;int[26]when the alphabet is small.- Min-heap of size k for top-k; monotonic stack/deque for next-greater and window extremes;
TreeMapnavigation for sorted queries. - Prefix sums in
long; hash sets for pairs; two pointers for sorted input; iterative BFS/DFS on adjacency lists. - Encode DP keys as
long; pickINFthat survives addition;1L << ipast 31.
Common questions
Why use ArrayDeque instead of Stack in Java?
Stack is a legacy class that extends the synchronised Vector; ArrayDeque is faster and the idiomatic choice — push, pop and peek as a stack, offer, poll and peek as a queue. It does not accept null elements.
What is a monotonic stack?
A stack of indexes kept in order of their values, used for next-greater-element and histogram problems. For each new element, pop every index whose value is smaller — the new element is their next greater — then push it. Each index is pushed and popped once, so the pass is O(n).
How do you count subarrays that sum to k in Java?
Keep a running prefix sum in a long and a HashMap from each prefix sum to how often it has occurred, starting with 0 seen once. At each element add the count of run - k to the answer, then record run. The whole pass is O(n).
How do you find the longest substring without repeating characters?
Use a sliding window: advance hi along the string, remember each character's last index in a map, and when the current character was last seen inside the window, move lo just past that position. The largest hi - lo + 1 is the answer, found in O(n).
What is TreeMap floorKey used for?
floorKey(x) returns the largest key less than or equal to x, and ceilingKey, lowerKey and higherKey give the other nearest keys, each in O(log n). They answer nearest-value and interval questions, such as whether a time slot is still free.
Exercises
Top-k frequent words
Read an integer n, n words, and k. Count with a frequency map, then keep the k most frequent words with a min-heap of size k (PriorityQueue ordered by count ascending, ties by word descending, so the alphabetically earlier word survives a tie). Print the survivors most frequent first, ties alphabetical, as <word> <count>.
Example: 7 then b a c a b a d then 2 →
a 3
b 2Sliding-window maximum
Read an integer n, n integers, and a window size w (1 ≤ w ≤ n). Print the maximum of every window of w consecutive elements, space-separated, in O(n) using a monotonic deque of indexes (values decreasing from front to back; drop indexes that leave the window from the front).
Example: 8 then 1 3 -1 -3 5 3 6 7 then 3 →
3 3 5 5 6 7In this module: Interview idioms
- The interview template — input, output, limits
- equals, hashCode and Comparable — the contracts that make collections work
- The pitfalls that fail interviews
- The collections idiom sheet (this lesson)
- Writing clean solutions under time pressure
- The Java theory drill — thirty questions, thirty answers
- Final checkpoint — Interview idioms
← The pitfalls that fail interviews · Writing clean solutions under time pressure →