Java for DSA
The subset of Java that earns its keep in coding interviews — the types that overflow, the collections that decide your complexity, and the small idioms you reach for in every problem.
After this you should be able to
- Choose interview-ready Java collections
- Avoid numeric overflow in solutions
Programming Fundamentals · Java for DSA
The subset of Java that actually earns its keep in coding interviews — the types that overflow, the collections that decide your complexity, and the small idioms you reach for in every problem.
You don't need the whole language. You need a small, sharp toolkit — the right
primitive type so math doesn't overflow, the right collection so lookups stay O(1), and a handful of
idioms (frequency arrays, computeIfAbsent, ArrayDeque as a stack) that turn a
wall of boilerplate into three lines. Master those, and the language stops being the obstacle.
Notebook Summary#
Types & Overflow#
- int → ±2.1B · long → ±9.2×10¹⁸ · char → 0–65535 · boolean, double
- mid →
left + (right - left) / 2(not(left+right)/2) - Use long when: prefix sums · n·(n−1) products · factorials · mod before overflow
(long)n * (n-1). (long)(n*(n-1)) already overflowed.
⚠ PITFALL -7 % 3 == -1 in Java. Positive mod →
((n%m)+m)%m.
Strings & Chars#
- Immutable → build with
StringBuilder(append loop O(n),+=loop O(n²)) - Compare:
.equals()for content · never== - Freq array:
freq[c - 'a']++→ 26 slots, beats HashMap for fixed alphabet .lengtharray (no ()) ·.length()string ·.size()list
Collections — pick by cost#
| Need | Use | Cost |
|---|---|---|
| dynamic array | ArrayList | O(1) get/add |
| lookup / count | HashMap/Set | O(1) avg |
| sorted keys | TreeMap/Set | O(log n) |
| BFS queue | ArrayDeque | O(1) |
| stack | ArrayDeque | O(1) |
| heap / top-K | PriorityQueue | O(log n) |
graph.computeIfAbsent(u, k->new ArrayList<>()).add(v) — adjacency list in one
line.
⚠ PITFALL list.remove(42) = remove
index 42. Value → remove(Integer.valueOf(42)).
💡 TIP Never use Stack (legacy, synchronized).
Use ArrayDeque as stack & deque.
Autoboxing#
Integer ==compares references; cache only −128..127- Unboxing
null→ NPE → usegetOrDefault(k, 0)
Integer a=200,b=200; a==b →
false. a==b for 100 → true. Silent bug.
Sorting / Comparators#
Arrays.sort(a, (x,y)->Integer.compare(x[0],y[0]))- descending:
Collections.reverseOrder()(objects only, notint[])
(a,b)->a-b overflows near
MIN/MAX. Use Integer.compare.
Recursion / Graphs#
- Call stack ~ few thousand frames · no tail-call optimization in Java
- skewed tree / deep DFS O(n) → risky for n > ~5–10k → go iterative w/ explicit stack
- Graph:
List<List>when nodes 0..n−1 ·HashMapwhen sparse/large IDs - Pair:
int[]{r,c}is the DSA default
new ArrayList<>(path) — a copy, not the live reference.
⚠ PITFALL Modify list inside for-each →
ConcurrentModificationException. Use iterator / backward index.
Idioms#
- MOD =
1_000_000_007· reduce every step:(sum+x)%MOD - ceil div:
(a + b - 1) / b - 4-dir:
{{-1,0},{1,0},{0,-1},{0,1}} - sentinels:
Integer.MAX_VALUE/MIN_VALUE - dedup sorted:
if(i>0 && a[i]==a[i-1]) continue;
Detailed Study Notes#
You open your binary search, run it on the tiny example, and it passes. You submit. It fails
one hidden case — a giant array — with a wrong answer, no crash, no stack trace. The bug:
(left + right) / 2 quietly overflowed int and wrapped to a negative index.
Nothing about Java told you. This is the whole reason a "just the language" chapter exists: the
parts of Java that decide whether your correct algorithm actually produces correct output are small,
specific, and easy to miss.
Imports: one line and move on#
Before any logic, your file needs to import the classes it uses — the compiler's way of knowing
where HashMap or PriorityQueue live. On LeetCode most of this is automatic,
but on a bare interview editor it is not, and fumbling imports wastes clock.
The wildcard java.util.* pulls in nearly everything DSA needs. Write it once at the top and
stop thinking about it.
import java.util.*; // ArrayList, HashMap, HashSet, Queue, PriorityQueue, etc.
import java.util.Arrays; // Arrays.sort(), Arrays.fill(), Arrays.asList()
import java.util.Collections; // Collections.sort(), Collections.reverseOrder()Types and the overflow trap#
Java is statically typed: every variable declares its type up front. For
problems, a handful of primitives cover almost everything. The one that bites is size — an
int holds roughly ±2.1 billion, and a sum or product can slip past that without warning.
An int is an odometer with a fixed number of digits. Drive past its maximum and it
doesn't stop — it rolls over to the lowest reading. Your math didn't error; the number silently
became something else entirely.
| Type | Size | Range | DSA use |
|---|---|---|---|
int | 4 B | −2.1B … 2.1B | indices, counters, most values |
long | 8 B | ±9.2×10¹⁸ | prefix sums, big products, mod math |
char | 2 B | 0 … 65,535 | characters, frequency arrays |
boolean | ~1 B | true / false | visited arrays, flags |
double | 8 B | ±1.7×10³⁰⁸ | geometry, rarely otherwise |
The classic trap is the binary-search midpoint. When left and right are both
near a billion, left + right exceeds int range and wraps negative
before the divide. Computing the gap first keeps every intermediate value in range:
// BUG: if left + right > Integer.MAX_VALUE, this overflows
int mid = (left + right) / 2;
// CORRECT: safe from overflow
int mid = left + (right - left) / 2;Reach for long whenever intermediate math can outgrow int: prefix sums over
large values (10⁵ elements each up to 10⁹ → 10¹⁴), products like n * (n - 1) near 10⁵,
factorials and combinations, and modulo problems where the running sum grows before you take the mod.
Placement of the cast is everything. (long) n * (n - 1) promotes n to
long first, so the multiply is 64-bit. (long)(n * (n - 1)) does the
multiply in int — it has already overflowed — then widens the corrupted
result. Same characters, wrong answer.
int wrong = n * (n - 1); // int overflow at n = 100000
long correct = (long) n * (n - 1); // cast first: safe 64-bit multiply
long stillWrong = (long)(n * (n - 1));// too late: int multiply already wrappedThe ternary operator is a compact inline if/else, handy for small choices:
int result = (a > b) ? a : b; // same as Math.max(a, b)
String label = (count == 1) ? "item" : "items";Operators: modulo, bits, and short-circuits#
Most operators behave as expected; a few carry interview-relevant surprises.
Modulo (%) can return negative results for negative operands —
-7 % 3 is -1, not 2. That breaks cyclic-index and hashing logic.
To force a non-negative remainder, use ((n % m) + m) % m.
Many problems ask for an answer "modulo 10⁹+7" to keep it inside int. Apply the mod
during computation, not just at the end, so the accumulator never overflows:
private static final int MOD = 1_000_000_007; // underscores are legal, just readability
long result = 0;
for (int i = 0; i < n; i++) {
result = (result + values[i]) % MOD; // reduce as you go
}
return (int) result;Bitwise operators unlock a whole class of problems. The XOR trick is the canonical example — XOR-ing every element cancels the pairs and leaves the lone value:
// nums = [4, 1, 2, 1, 2] — every value appears twice except one
int single = 0;
for (int num : nums) single ^= num; // duplicates cancel
// single = 4| Op | Sym | DSA use |
|---|---|---|
| AND | & | masking, test a bit: (n & (1<<i)) != 0 |
| OR | | | set a bit: n | (1<<i) |
| XOR | ^ | unique elements, toggling |
| NOT | ~ | bit inversion |
| Left shift | << | 1<<n = 2ⁿ |
| Right shift | >> | ÷ by 2ⁿ, keeps sign |
| Unsigned rsh | >>> | ÷ by 2ⁿ, fills with 0 |
boolean isSet = (mask & (1 << i)) != 0; // check bit i
mask = mask | (1 << i); // set bit i
mask = mask & ~(1 << i); // clear bit i
mask = mask ^ (1 << i); // toggle bit i
int count = Integer.bitCount(mask); // count set bitsShort-circuit evaluation with && and || stops as soon
as the result is decided, left to right. Order your conditions so the bounds check protects the access:
if (index < nums.length && nums[index] == target) is safe: the array read
only runs when the index is in range. Flip the two conditions and you get an
ArrayIndexOutOfBoundsException.
Methods and pass-by-value#
Java is always pass-by-value — but for objects, the value passed is the reference. That one sentence explains a surprising amount of behavior.
Passing an object hands the method a photocopy of the address of a house, not a copy of the house. The method can walk in and rearrange the furniture (the caller sees it). But if it scribbles a new address on its copy, the caller's original address is untouched.
- Primitives are copied — changing a parameter inside a method never touches the caller.
- Objects / arrays pass a copied reference — you can mutate the contents
(
arr[i] = 5), but reassigning (arr = new int[10]) is invisible outside.
This is why an array swap helper works, and why you cannot write a method that swaps two
int variables. It is also the engine of backtracking: a shared List is mutated
down each branch and restored on the way back up.
private void backtrack(List<Integer> path, List<List<Integer>> results) {
if (isComplete(path)) {
results.add(new ArrayList<>(path)); // MUST copy, not add the reference
return;
}
for (int choice : choices) {
path.add(choice); // modify shared list
backtrack(path, results); // recurse
path.remove(path.size() - 1); // undo
}
}results.add(path) stores a pointer to the one shared list. As backtracking unwinds and
empties path, every saved entry empties too — you end up with a list of identical empty
lists. new ArrayList<>(path) snapshots the current state.
Fig. 1 — shared reference collapses to duplicates; copies preserve each snapshot.
Autoboxing and its traps#
Collections can't hold primitives, so Java auto-wraps int as
Integer, char as Character, and back again. That
conversion is convenient and mostly invisible — which is exactly why its failure modes are sneaky.
Java caches boxed Integer objects for values −128 to 127. So
Integer a = 100, b = 100; a == b is true, but
Integer a = 200, b = 200; a == b is false — different objects. It
passes on small test inputs and breaks on large ones. Always compare with .equals() or
unbox to int first.
Integer a = 200, b = 200;
a == b; // false — reference comparison
a.equals(b); // true — value comparison
Integer c = 100, d = 100;
c == d; // true — both from the −128..127 cache (misleading!)Two more sharp edges: unboxing a null throws NullPointerException, so prefer
map.getOrDefault(key, 0) over a raw get that might return null. And each
boxing allocates a heap object — in a tight loop over millions of elements, prefer primitive
int[] to ArrayList<Integer>.
Arrays and the frequency-count trick#
Arrays are fixed-size, zero-indexed, and the default data structure of almost every
problem. Default values differ by type and quietly matter: int[] starts at
0, boolean[] at false (perfect for visited arrays), object arrays
like String[] at null (must initialize before use).
Array size is a property: arr.length, no parentheses. String length is a method:
s.length(). List size is list.size(). Mixing these up is one of the most
common Java compile errors under time pressure.
DP tables often need one extra row/column for the base case (new int[n + 1]). And matrix
problems lean on a directions array to visit orthogonal neighbors without four
copy-pasted blocks:
int[][] directions = {{-1,0}, {1,0}, {0,-1}, {0,1}};
for (int[] dir : directions) {
int nr = row + dir[0], nc = col + dir[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
// process neighbor (nr, nc)
}
}
// diagonals too? add {-1,-1},{-1,1},{1,-1},{1,1}Fig. 2 — one center cell, four offset pairs: up, down, left, right.
The Arrays utility class carries the workhorses: sort, fill,
copyOf, copyOfRange, and toString/deepToString for
debugging.
Arrays.asList("a","b") returns a fixed-size list backed by the array —
add/remove throw UnsupportedOperationException. Wrap it:
new ArrayList<>(Arrays.asList(...)). And it does not expand a primitive
int[] into a List<Integer>; it makes a one-element
List<int[]>.
Strings: immutable, so build them right#
Java strings are immutable — every modification creates a brand-new object.
Concatenating in a loop with += therefore rebuilds the whole string each pass, turning an
O(n) task into O(n²).
String += in a loop is like retyping an entire letter from scratch every time you want
to add one word. StringBuilder is a whiteboard: you just append to what's already
there, then read it off once at the end.
// BAD: O(n^2) — each += allocates a new String
String result = "";
for (char c : chars) result += c;
// GOOD: O(n) — one mutable buffer
StringBuilder sb = new StringBuilder();
for (char c : chars) sb.append(c);
String result = sb.toString();Compare strings by content with .equals(). The == operator compares
object identity; it appears to work for pooled literals but fails for strings built at runtime — an
unreliable source of subtle bugs.
The highest-leverage string idiom is the character-to-index map. Because chars are
numbers, c - 'a' maps 'a'..'z' to 0..25, giving you a 26-slot
frequency array with no HashMap:
int[] freq = new int[26];
for (char c : s.toCharArray()) {
freq[c - 'a']++; // freq[0]='a', freq[1]='b', ...
}When the character set is bounded (lowercase, uppercase, or digits), an int[26]
frequency array is faster and lighter than HashMap<Character,Integer> — no
hashing, no boxing. Same pattern gives digit extraction with c - '0' and reverse
mapping with (char)('a' + index).
Round out the toolkit with StringBuilder's insert, reverse,
deleteCharAt, and setCharAt, plus the Character tests:
isLetterOrDigit, isDigit, toLowerCase, and friends.
The Collections Framework#
Choosing the right collection is often the difference between an O(n) and an O(n²)
solution. The framework splits into three families: ordered sequences (List),
unique membership (Set), and key–value lookup (Map), plus queue/stack shapes.
Fig. 3 — List / Set / Queue descend from Collection; Map is its own hierarchy.
ArrayList — the result list#
Amortized O(1) append and O(1) index access; O(n) search and removal. The two remove
overloads are a classic trap.
On a List<Integer>, list.remove(42) removes the element at
index 42. To remove the value 42, write
list.remove(Integer.valueOf(42)).
HashMap & HashSet — O(1) lookups#
The most-used pair in all of DSA. HashMap gives average O(1) put/get/containsKey;
HashSet gives O(1) membership. Frequency counting is the signature pattern:
Map<Character, Integer> freq = new HashMap<>();
for (char c : s.toCharArray()) {
freq.put(c, freq.getOrDefault(c, 0) + 1);
}set.add(x) returns false if x was already present. So
if (!seen.add(num)) { /* duplicate */ } detects duplicates in a single operation — no
separate contains call.
LinkedHashMap / TreeMap — order and sorting#
LinkedHashMap preserves insertion order and, in access-order mode, is the backbone of an LRU
cache (override removeEldestEntry to auto-evict). TreeMap/TreeSet
keep keys sorted with O(log n) navigation — floorKey, ceilingKey,
lowerKey, higherKey, headMap, tailMap,
subMap — ideal for range queries and sorted-window problems.
Queue & Deque — BFS and the "better stack"#
Every BFS starts with a queue. Capture queue.size() before the inner loop when you need
level-order processing:
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{startRow, startCol});
while (!queue.isEmpty()) {
int size = queue.size(); // freeze this level's count
for (int i = 0; i < size; i++) {
int[] cell = queue.poll();
// process cell, enqueue neighbors
}
}offer/poll return false/null on failure instead
of throwing — usually what you want in DSA. And never use Stack: it extends the legacy,
synchronized Vector. Use ArrayDeque for both stacks
(push/pop/peek) and double-ended queues
(offerFirst/pollLast).
PriorityQueue — heaps#
A min-heap by default; wrap with Collections.reverseOrder() or a comparator for max-heaps
and custom orders. O(log n) insert/remove, O(1) peek. Used for top-K, k-way merge, and Dijkstra.
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));pq.remove(element) scans the heap linearly. If you must delete arbitrary elements, use a
TreeMap, or use lazy deletion: mark items stale and skip them when they
surface via poll().
| Collection | Key methods | Complexity | Use for |
|---|---|---|---|
ArrayList | add, get, set | O(1) get/add | result lists |
HashMap | put, getOrDefault | O(1) avg | counting, lookups |
HashSet | add, contains | O(1) avg | visited, dedup |
TreeMap | floorKey, ceilingKey | O(log n) | sorted, range queries |
ArrayDeque | push, pop, offerLast | O(1) | stacks, deques |
PriorityQueue | offer, poll, peek | O(log n) | heaps, top-K, Dijkstra |
Sorting and comparators#
A comparator returns negative if the first element should come first, zero if equal,
positive if it should come after. Java sorts primitives with Arrays.sort and
objects/lists with a lambda comparator.
// sort intervals by start (merge-intervals pattern)
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
// multi-key: length, then lexicographic
Arrays.sort(words, Comparator.comparingInt(String::length).thenComparing(Comparator.naturalOrder()));(a, b) -> a - b reads cleanly but overflows when values are near
Integer.MIN/MAX — the difference wraps and the sort order flips. Always use
Integer.compare(a, b).
For descending order, Collections.reverseOrder() works on Integer[] and lists —
but not on primitive int[]. For a primitive array, sort ascending and reverse
manually, or switch to Integer[].
Functional shortcuts & null handling#
A few Java 8 map methods collapse common boilerplate. The standout is computeIfAbsent for
building adjacency lists:
Map<Integer, List<Integer>> graph = new HashMap<>();
graph.computeIfAbsent(node, k -> new ArrayList<>()).add(neighbor);
// also: map.merge(key, 1, Integer::sum) — increment a counter
// map.putIfAbsent(key, value) — set only if absentnull is the top runtime-error source in DSA, showing up in tree/linked-list traversal, map
lookups, and uninitialized object arrays. Three rules cover most cases: check for null before
dereferencing (if (node.left != null)), prefer getOrDefault over
get, and combine null + empty guards at the top of your solution
(if (nums == null || nums.length == 0) return ...;).
Modifying collections while iterating#
Removing from a collection inside a for-each loop throws ConcurrentModificationException —
the iterator detects the structural change mid-walk.
Three safe fixes: (1) use an explicit Iterator and call it.remove(); (2)
collect items to remove in a second list, then removeAll after the loop; (3) for an
ArrayList, iterate backward by index so removals don't shift unseen elements.
// safest: Iterator.remove()
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
if (it.next() % 2 == 0) it.remove();
}Recursion and the call stack#
Every method call pushes a frame onto the call stack, which holds only a few thousand
frames. Recurse too deep and you get a StackOverflowError — and Java, unlike
some functional languages, does not optimize tail calls, so every recursive call adds a frame
regardless.
| Scenario | Depth | Risk |
|---|---|---|
| Balanced tree (n nodes) | O(log n) | safe to n ≈ 10⁶ |
| Skewed tree / linked list | O(n) | risky if n > 5–10k |
| Backtracking (depth d) | O(d) | usually safe |
| DFS on graph (n nodes) | O(n) | risky for large n |
When depth scales with input and the input can be large, convert recursion to iteration with an explicit
ArrayDeque stack — same traversal, no frame limit.
void dfs(int start, boolean[] visited) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(start);
while (!stack.isEmpty()) {
int node = stack.pop();
if (visited[node]) continue;
visited[node] = true;
for (int nb : graph.get(node)) {
if (!visited[nb]) stack.push(nb);
}
}
}Graph representation#
Java has no built-in graph type — you build the adjacency list yourself. Two shapes cover nearly everything, chosen by how the nodes are numbered.
List<List<Integer>>#
- fast index access, no hashing
- nodes are dense: 0 … n−1
- preallocate n inner lists
HashMap<Integer, List>#
- handles sparse / large / non-numeric IDs
- slight hashing overhead
- pairs with
computeIfAbsent
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
for (int[] e : edges) {
graph.get(e[0]).add(e[1]);
graph.get(e[1]).add(e[0]); // omit for directed graphs
}Weighted graphs store int[]{neighbor, weight} pairs, feeding a comparator-ordered
PriorityQueue in Dijkstra.
Pairs, dedup, and everyday idioms#
Java has no built-in Pair. In practice, int[]{row, col} is the standard — fast
and concise, at the cost of type safety. Use a small custom class only when clarity genuinely suffers.
For deduplication, the sorted-array approach is preferred (used in 3Sum/4Sum): sort, then skip equal neighbors.
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
if (i> 0 && nums[i] == nums[i - 1]) continue; // skip duplicate
// process nums[i]
}A grab-bag worth memorizing: ceiling division without floats is (a + b - 1) / b; sentinels
are Integer.MAX_VALUE / MIN_VALUE; and deep-copying a 2D array needs a per-row
clone() because a plain clone() copies only the outer array of shared row
references.
-Integer.MIN_VALUE overflows — the positive range is one smaller than the negative. If
you might negate a value that could be MIN_VALUE, do it in long. The same
asymmetry breaks Math.abs(Integer.MIN_VALUE).
Node classes for design problems#
DSA problems define custom nodes — a ListNode for linked lists, a TreeNode for
trees. Fields are left package-private with no getters/setters: this is interview code under time
pressure, not production software, so encapsulation adds no value.
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) { this.val = val; }
}The two-argument ListNode constructor lets you build a list inline —
new ListNode(1, new ListNode(2, new ListNode(3))) — which LeetCode often provides for you.
this disambiguates the field from the same-named parameter in the constructor.
Interview & Practice Q&A#
Why does (left + right) / 2 overflow, and what's the fix?
When both are near Integer.MAX_VALUE, their sum exceeds the int
range and wraps to a negative number before the divide, producing a negative or wrong
midpoint. Compute the gap first: left + (right - left) / 2 keeps every
intermediate value in range and yields the same midpoint.
Why can Integer a = 200; Integer b = 200; a == b be false?
== compares object references, not values. Java caches boxed
Integer objects only for −128 to 127, so small values share one cached object
(== is true) while larger values are distinct objects (== is
false). Always compare with .equals() or unbox to int.
Explain "pass-by-value" for objects in Java.
Java always passes a copy of the value. For an object, that value is the reference (an address), so the method receives a copy of the reference. It can mutate the object's contents through it — visible to the caller — but reassigning the parameter to a new object does not affect the caller's variable.
Why is building a string with += in a loop O(n²)?
Strings are immutable, so each += allocates a new string and copies all existing
characters into it. Over n appends that's 1+2+…+n copies, which is O(n²).
StringBuilder mutates one buffer in place for amortized O(n) total.
When would you pick int[] over ArrayList<Integer>?
When performance matters in tight loops over large inputs.
ArrayList<Integer> boxes every element into a heap object and unboxes on
read, adding allocation and indirection. A primitive int[] avoids both, at the
cost of a fixed size and no built-in growth.
Why avoid Stack, and what replaces it?
Stack extends the legacy Vector, which synchronizes every operation
— unnecessary overhead in single-threaded interview code — and exposes index-based methods
that break the stack abstraction. Use ArrayDeque via
push/pop/peek; it's faster and also serves as a
double-ended queue.
How do you detect a duplicate in one operation with a Set?
set.add(x) returns false when x was already present.
So if (!seen.add(num)) { /* duplicate */ } both records the element and reports
whether it was new — no separate contains check.
What causes ConcurrentModificationException and how do you avoid it?
Structurally modifying a collection (add/remove) while iterating it with a for-each loop; the
iterator notices the change and throws. Fix it with an explicit Iterator and
it.remove(), by collecting removals and calling removeAll
afterward, or by iterating an ArrayList backward by index.
How do you remove an arbitrary element from a PriorityQueue efficiently?
You can't directly — pq.remove(element) is O(n) because the heap isn't
searchable by value. Use lazy deletion (mark the element stale and skip it when it surfaces
via poll()), or switch to a TreeMap/TreeSet when you
need ordered structure with O(log n) removal.
Key Takeaways#
- Overflow is silent. Use
longfor sums/products that can exceed ±2.1B, cast before multiplying, and write midpoints asleft + (right - left) / 2. - The collection is the complexity. HashMap/HashSet for O(1) lookup, TreeMap for sorted O(log n), ArrayDeque for stacks and queues, PriorityQueue for heaps.
- Wrappers lie about equality. Compare
IntegerandStringwith.equals(); guard against unboxingnullwithgetOrDefault. - Immutable strings need
StringBuilder; fixed alphabets need afreq[c - 'a']array, not a HashMap. - Pass-by-value + references is why backtracking must save
new ArrayList<>(path), not the live list. - Deep recursion overflows the stack (no tail-call optimization) — convert to an explicit-stack loop when depth scales with n.