Study Notes
Foundationsfoundation

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.

35 min read·verified Java 21·reviewed 2026-08-30

After this you should be able to

  • Choose interview-ready Java collections
  • Avoid numeric overflow in solutions
OverflowCollectionsIdioms

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.

Updated July 2026 · Read ~22 min · DSA 101

// the big idea

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#

Hand-copy this. Every line is derivable from the detailed notes below.

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
⭐ TRICK Cast before multiply: (long)n * (n-1). (long)(n*(n-1)) already overflowed. ⚠ PITFALL -7 % 3 == -1 in Java. Positive mod → ((n%m)+m)%m.
MAX −neg
doodle: line runs to MAX, then loops back around to negative

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
  • .length array (no ()) · .length() string · .size() list

Collections — pick by cost#

NeedUseCost
dynamic arrayArrayListO(1) get/add
lookup / countHashMap/SetO(1) avg
sorted keysTreeMap/SetO(log n)
BFS queueArrayDequeO(1)
stackArrayDequeO(1)
heap / top-KPriorityQueueO(log n)
⭐ TRICK 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 → use getOrDefault(k, 0)
⚠ PITFALL 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, not int[])
⚠ PITFALL (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 · HashMap when sparse/large IDs
  • Pair: int[]{r,c} is the DSA default
⭐ TRICK Backtracking: save new ArrayList<>(path) — a copy, not the live reference. ⚠ PITFALL Modify list inside for-each → ConcurrentModificationException. Use iterator / backward index.
add() recurse undo
doodle: add → recurse → undo, looping back for the next choice

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.

1.1

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.

imports.java
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()
1.2

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.

Analogy · car odometer

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.

TypeSizeRangeDSA use
int4 B−2.1B … 2.1Bindices, counters, most values
long8 B±9.2×10¹⁸prefix sums, big products, mod math
char2 B0 … 65,535characters, frequency arrays
boolean~1 Btrue / falsevisited arrays, flags
double8 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.

⭐ Trick · cast before you multiply

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 wrapped

The 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";
1.3

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
OpSymDSA 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
bit toolkit
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 bits

Short-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:

💡 Tip · guard order matters

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.

1.4

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.

Analogy · a photocopy of an address

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.

backtracking skeleton
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
    }
}
⚠ Pitfall · adding the live reference

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.

results.add(path) — all point to one live list r[0] r[1] r[2] [] (emptied) new ArrayList<>(path) — independent copies r[0] [1] r[1] [1,2] r[2] [1,3]

Fig. 1 — shared reference collapses to duplicates; copies preserve each snapshot.

1.5

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.

⚠ Pitfall · == compares Integer references

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>.

1.6

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).

⚠ Pitfall · .length vs .length() vs .size()

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:

4-directional neighbors
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}
r,c [-1,0] [+1,0] [0,-1] [0,+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.

⚠ Pitfall · Arrays.asList

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[]>.

1.7

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²).

Analogy · retyping the letter

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:

frequency array
int[] freq = new int[26];
for (char c : s.toCharArray()) {
    freq[c - 'a']++;   // freq[0]='a', freq[1]='b', ...
}
⭐ Trick · fixed alphabet beats HashMap

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.

1.8

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.

Collection List Set Queue/Deque ArrayList LinkedList HashSet TreeSet ArrayDeque PriorityQueue Map (separate) HashMap · TreeMap · LinkedHashMap

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.

⚠ Pitfall · remove(int) vs remove(Object)

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:

frequency counting
Map<Character, Integer> freq = new HashMap<>();
for (char c : s.toCharArray()) {
    freq.put(c, freq.getOrDefault(c, 0) + 1);
}
⭐ Trick · Set.add returns a duplicate flag

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:

level-order BFS
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
    }
}
💡 Tip · offer/poll over add/remove, and skip Stack

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]));
⚠ Pitfall · arbitrary removal is O(n)

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().

CollectionKey methodsComplexityUse for
ArrayListadd, get, setO(1) get/addresult lists
HashMapput, getOrDefaultO(1) avgcounting, lookups
HashSetadd, containsO(1) avgvisited, dedup
TreeMapfloorKey, ceilingKeyO(log n)sorted, range queries
ArrayDequepush, pop, offerLastO(1)stacks, deques
PriorityQueueoffer, poll, peekO(log n)heaps, top-K, Dijkstra
1.9

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()));
⚠ Pitfall · the subtraction comparator

(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[].

1.10

Functional shortcuts & null handling#

A few Java 8 map methods collapse common boilerplate. The standout is computeIfAbsent for building adjacency lists:

adjacency list in one line
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 absent

null 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 ...;).

1.11

Modifying collections while iterating#

Removing from a collection inside a for-each loop throws ConcurrentModificationException — the iterator detects the structural change mid-walk.

⚠ Pitfall · remove inside for-each

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();
}
1.12

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.

ScenarioDepthRisk
Balanced tree (n nodes)O(log n)safe to n ≈ 10⁶
Skewed tree / linked listO(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.

iterative DFS
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);
        }
    }
}
1.13

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
dense: nodes 0..n-1
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.

1.14

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.

⚠ Pitfall · negating MIN_VALUE

-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).

1.15

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.

canonical node classes
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#

The follow-ups this material naturally invites. Answer each aloud before expanding.

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#

  1. Overflow is silent. Use long for sums/products that can exceed ±2.1B, cast before multiplying, and write midpoints as left + (right - left) / 2.
  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.
  3. Wrappers lie about equality. Compare Integer and String with .equals(); guard against unboxing null with getOrDefault.
  4. Immutable strings need StringBuilder; fixed alphabets need a freq[c - 'a'] array, not a HashMap.
  5. Pass-by-value + references is why backtracking must save new ArrayList<>(path), not the live list.
  6. Deep recursion overflows the stack (no tail-call optimization) — convert to an explicit-stack loop when depth scales with n.
overflow · cast-first left+(right−left)/2 ((n%m)+m)%m StringBuilder freq[c−'a'] .equals() not == getOrDefault computeIfAbsent ArrayDeque = stack offer / poll PriorityQueue TreeMap floor/ceiling Integer.compare new ArrayList<>(path) iterative DFS int[]{r,c} directions[][] MOD 1e9+7