Java Tricks & Idioms
Scope — The Java language semantics that decide whether a correct algorithm produces a correct answer: characters as integers, value vs reference, and integer arithmetic. The library APIs live in two companion sheets. See also: java_trick_collections.md — arrays, lists, maps, queues, heaps, stacks and pairs; java_trick_strings_sorting.md — String and StringBuilder work, and everything comparator-shaped; python_trick.md — the same ground in Python; complexity_cheatsheet.md — the Big-O lookup tables for every structure these APIs wrap.
LeetCode Problem Lists
Overview
Three families of bug account for almost every “the logic was right but the answer was wrong” in Java interviews, and all three are language semantics rather than algorithm choices:
| Family | The bug | Fixed in |
|---|---|---|
| Characters are integers | charAt(i) gives you a char, and using it where you meant a digit silently gives you its ASCII code |
Characters & Digits |
| Value vs reference | you mutate an object you thought you had copied, or you copy a primitive you thought you were sharing | Value vs Reference |
| Integer arithmetic | / truncates toward zero, % can return a negative, and int overflows at 2^31 |
Integer Math & Operators |
References
Characters & Digits
charAt, comparisons and letter → index
Key Methods: charAt(), character comparisons, ASCII operations
// Basic character access
String s = "www.google.com";
char result = s.charAt(6); // Returns 'g'
// Character comparison in palindrome check (LC 647)
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
l--;
r++;
count++;
}
// Character to index mapping (lowercase a-z)
char c = 'c';
int index = c - 'a'; // Returns 2 (c is 3rd letter, 0-indexed)
// java
String x = "abcxyz";
for ( Character c: x.toCharArray()){
/**
* c = a, a - ? = 0
* c = b, a - ? = -1
* c = c, a - ? = -2
* c = x, a - ? = -23
* c = y, a - ? = -24
* c = z, a - ? = -25
*/
System.out.println(" c = " + c +
", a - ? = " + ('a' - c)
);
}
Performance Note: charAt(i) is O(1) for strings, making it efficient for character-by-character processing.
Char digit to integer value (char - '0')
Key Concept: Convert a character digit (‘0’-‘9’) to its integer value using ASCII subtraction.
// Basic conversion: char digit → int value
char c = '3';
int value = c - '0'; // value = 3
// Why it works:
// '0' = 48 (ASCII)
// '1' = 49
// '2' = 50
// '3' = 51 → '3' - '0' = 51 - 48 = 3
// ...
// '9' = 57 → '9' - '0' = 57 - 48 = 9
Common Pattern: Increment/Decrement Digit Characters (LC 752 - Open the Lock)
// LC 752 - Open the Lock
// Rotate lock wheel: '0' → '1' → '2' ... '9' → '0' (wrap around)
String cur = "0000";
for (int i = 0; i < 4; i++) {
char c = cur.charAt(i);
// Get integer value from char
int digit = c - '0'; // e.g., '3' → 3
// Calculate rotations with wrap-around
int valPlus = (digit + 1) % 10; // 9 → 0 (wrap)
int valMinus = (digit - 1 + 10) % 10; // 0 → 9 (wrap)
// Convert back to char for new string
char charPlus = (char) ('0' + valPlus); // 4 → '4'
char charMinus = (char) ('0' + valMinus); // 2 → '2'
// Build new lock combinations
String str1 = cur.substring(0, i) + charPlus + cur.substring(i + 1);
String str2 = cur.substring(0, i) + charMinus + cur.substring(i + 1);
}
Quick Reference Table:
| Operation | Code | Example |
|---|---|---|
| Char → Int | c - '0' |
'7' - '0' → 7 |
| Int → Char | (char)('0' + n) |
(char)('0' + 7) → '7' |
| Increment (wrap) | (digit + 1) % 10 |
9 + 1 → 0 |
| Decrement (wrap) | (digit - 1 + 10) % 10 |
0 - 1 → 9 |
Why + 10 in decrement?
// Without +10: (0 - 1) % 10 = -1 ❌ (negative!)
// With +10: (0 - 1 + 10) % 10 = 9 ✅
// The +10 ensures the value is always positive before modulo
int valMinus = (digit - 1 + 10) % 10;
Comparison: Letter vs Digit Mapping
| Type | Char → Index | Index → Char | Range |
|---|---|---|---|
| Letters | c - 'a' |
(char)('a' + i) |
‘a’-‘z’ → 0-25 |
| Digits | c - '0' |
(char)('0' + i) |
‘0’-‘9’ → 0-9 |
charAt() returns a char, not the digit value Priority 1 of 5 — Nice to have
Pitfall:
new Integer(s.charAt(i))gives the ASCII code (e.g. 51), not the digit (e.g. 3).
Wrong
int val1 = new Integer(s.charAt(i));
// s.charAt(i) = '3' → '3' is char → ASCII 51
// val1 = 51 ❌
Correct
int val1 = s.charAt(i) - '0';
// '3' - '0' = 51 - 48 = 3 ✅
Why: char is stored as ASCII/Unicode value
| Expression | Result |
|---|---|
'3' |
51 (ASCII) |
'0' |
48 |
'3' - '0' |
3 |
new Integer(...) is also deprecated
new Integer(...) // ❌ Deprecated, unnecessary object creation, slower
Integer.valueOf(...) // ✅ If you need an Integer object
int x = ...; // ✅ Prefer primitives
For multi-digit substrings, parseInt is correct
int val2 = Integer.parseInt(s.substring(i - 2, i));
// ✅ Correct — parsing a String, not a char
TL;DR
new Integer(s.charAt(i)) // ❌ gives ASCII (e.g. 51)
s.charAt(i) - '0' // ✅ gives actual digit (e.g. 3)
Mapping letters to array indices (c - 'a')
Because char is an integer type, subtracting 'a' turns a lowercase letter into a
0-based index, which is what lets a 26-slot array replace a HashMap<Character, ?>:
// java
// rank each letter of a custom alphabet -- LC 269 Alien Dictionary
String order = "hlabcdefgijkmnopqrstuvwxyz";
int[] orderMap = new int[26];
for (int i = 0; i < order.length(); i++) {
orderMap[order.charAt(i) - 'a'] = i; // 'h' - 'a' == 7 -> orderMap[7] = 0
}
The reason to bother is complexity, not brevity: the array read is O(1), whereas the obvious
alternative order.indexOf(c) rescans the string and is O(n) on every lookup. Inside a
comparison loop that is the difference between O(n) and O(n²).
The same subtraction applies to digits with '0' instead of 'a' — see the letter-vs-digit
table above, which is the one to memorise.
Iterating a character range
// Iterate through lowercase alphabet
for (char c = 'a'; c <= 'z'; c++) {
System.out.print(c + " "); // Output: a b c d e f g h i j k l m n o p q r s t u v w x y z
}
// Iterate through uppercase alphabet
for (char c = 'A'; c <= 'Z'; c++) {
System.out.print(c + " "); // Output: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
}
// Generate all single-character replacements (LC 127)
String word = "hit";
for (int i = 0; i < word.length(); i++) {
for (char c = 'a'; c <= 'z'; c++) {
if (c != word.charAt(i)) {
String newWord = word.substring(0, i) + c + word.substring(i + 1);
// Process newWord
}
}
}
Value vs Reference — the rule behind most Java bugs here
equals() vs == — when to use which Priority 1 of 5 — Nice to have
Core Rule:
==compares references (are they the same object?).equals()compares content (do they have the same value?).
The Rule — equals() vs ==
== → compares memory addresses (reference identity)
equals() → compares logical content (value equality)
Comparing Collections (List, Set, Map)
// LC 872 — Leaf-Similar Trees
List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
getLeafSeq(root1, list1);
getLeafSeq(root2, list2);
/** NOTE !!!
*
* Use `equals()` to compare two lists by content.
* `==` would check if they are the SAME object (always false here).
*/
list1 == list2 // ❌ false — different objects, even if same content
list1.equals(list2) // ✅ true — compares element-by-element
Comparing Strings
String a = new String("hello");
String b = new String("hello");
a == b // ❌ false — different objects
a.equals(b) // ✅ true — same content
// BUT: string literals from pool may share reference
String c = "hello";
String d = "hello";
c == d // ✅ true (same pooled object) — but DON'T rely on this!
c.equals(d) // ✅ true — always use this
Comparing Wrapper Types (Integer, Long, etc.)
Integer x = 127;
Integer y = 127;
x == y // ✅ true (cached: -128 to 127)
Integer a = 128;
Integer b = 128;
a == b // ❌ false — outside cache range, different objects!
a.equals(b) // ✅ true — always correct
// SAFE alternative for null-safe comparison
Objects.equals(a, b) // ✅ handles null without NPE
Comparing Primitives
int i = 5;
int j = 5;
i == j // ✅ correct — primitives have no .equals()
// Primitives are ALWAYS compared by value with ==
Summary Table — equals() vs ==
| Type | Use == |
Use equals() |
Pitfall |
|---|---|---|---|
int, long, char, … (primitives) |
Yes | N/A (no method) | None |
String |
No | Yes | Literals may share ref, but don’t rely on it |
Integer, Long, … (wrappers) |
No | Yes | == works for -128…127 only (cache) |
List, Set, Map |
No | Yes | == checks identity, not content |
| Custom objects | No | Yes (if overridden) | Default equals() is same as == |
null check |
Yes (x == null) |
No (NPE!) | Always use == for null check |
Interview Quick Rule
Q: "Should I use == or equals()?"
→ Is it a primitive (int, long, boolean, char, double)?
YES → use ==
→ Is it checking for null?
YES → use == (x == null)
→ Everything else (String, Integer, List, Object...)?
→ use equals()
→ use Objects.equals(a, b) if either could be null
Primitive vs reference types in recursion — the backtracking rule Priority 1 of 5 — Nice to have
Core Rule: Primitives are pass-by-value → each call gets its own copy → no backtracking needed. Reference types (collections, arrays, objects) are pass-by-reference → shared state → must backtrack.
The Rule — primitives vs references in recursion
Primitive param (int, long, double...) → copy per call → NO backtrack needed
Reference param (List, int[], HashMap) → shared object → MUST backtrack (add + remove)
Global / instance variable → shared state → MUST backtrack
Case 1: Primitive — No Backtracking Needed (LC 112 Path Sum)
// LC 112 - Path Sum
// curSum is a primitive int → each recursive call gets its OWN COPY
// → no backtracking needed
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null) return false;
if (root.left == null && root.right == null) return root.val == targetSum;
getPathHelper(root, 0);
return pathSumMap.containsValue(targetSum);
}
private void getPathHelper(TreeNode root, Integer curSum) {
if (root == null) return;
int newSum = curSum + root.val; // new local variable — does NOT affect parent's curSum
if (root.left == null && root.right == null) {
pathSumMap.put(newSum, newSum);
}
/** NOTE !!!
*
* No backtrack on curSum / newSum needed!
*
* Reason:
* - curSum is a primitive (int / Integer with autoboxing creates a new object).
* - Each recursive call receives its OWN COPY of curSum.
* - Modifying newSum inside the call does NOT affect the parent's curSum.
* - When the call returns, the parent's curSum is completely unchanged.
*
* Stack visualization for tree 5 -> 4 -> 11 -> 7:
*
* getPathHelper(5, curSum=0) newSum=5
* getPathHelper(4, curSum=5) newSum=9
* getPathHelper(11, curSum=9) newSum=20
* getPathHelper(7, curSum=20) newSum=27 ← leaf, store 27
* ← returns, parent curSum still 20 ✅
* ← returns, parent curSum still 9 ✅
* ← returns, parent curSum still 5 ✅
*/
getPathHelper(root.left, newSum);
getPathHelper(root.right, newSum);
}
Memory model:
Stack frame: getPathHelper(node=4, curSum=5)
├── newSum = 9 ← local to THIS frame
├── calls getPathHelper(node.left, newSum=9)
│ └── newSum = 20 ← different frame, isolated
└── curSum is still 5 when left call returns ✅
Case 2: Global Variable — Backtracking IS Needed (LC 112 V0-2)
// BAD pattern: using an instance variable for path sum
// → ALL recursive calls share the SAME curSum → must backtrack!
private int curSum = 0; // shared across ALL calls ← danger!
public boolean hasPathSum_0_2(TreeNode root, int targetSum) {
curSum += root.val; // modifies shared state
if (root.left == null && root.right == null) {
if (curSum == targetSum) {
curSum -= root.val; // MUST backtrack before returning
return true;
}
}
if (hasPathSum_0_2(root.left, targetSum)) {
curSum -= root.val; // MUST backtrack
return true;
}
if (hasPathSum_0_2(root.right, targetSum)) {
curSum -= root.val; // MUST backtrack
return true;
}
curSum -= root.val; // MUST backtrack on failure path too
return false;
}
Case 3: Reference Type (List) — Backtracking IS Needed (LC 113 Path Sum II)
// List is passed by reference → shared object → must backtrack
private void dfs(TreeNode node, int remain, List<Integer> path, List<List<Integer>> res) {
if (node == null) return;
path.add(node.val); // ← mutates shared list
if (node.left == null && node.right == null && remain == node.val) {
res.add(new ArrayList<>(path)); // snapshot before backtrack
} else {
dfs(node.left, remain - node.val, path, res);
dfs(node.right, remain - node.val, path, res);
}
path.remove(path.size() - 1); // ← MUST backtrack: undo the add
}
Case 4: StringBuilder — Backtracking IS Needed (LC 988 Smallest String Starting From Leaf)
// LC 988 - Smallest String Starting From Leaf
// StringBuilder is a reference type → shared object → MUST backtrack
private String smallest = "";
public String smallestFromLeaf(TreeNode root) {
dfs(root, new StringBuilder());
return smallest;
}
private void dfs(TreeNode node, StringBuilder sb) {
if (node == null)
return;
/** NOTE !!!
*
* PRE-ORDER DFS: process current node first
*/
// 1. Add current character (0 -> 'a', 1 -> 'b', etc.)
sb.append((char) ('a' + node.val));
/** NOTE !!!
*
* ONLY treat as result when reach `leaf`
*/
// 2. Leaf check: If we reach a leaf, we have a candidate path
if (node.left == null && node.right == null) {
/** NOTE !!!
*
* We reverse current StringBuilder to fit the requirement
* (string from leaf to root)
*/
String currentStr = new StringBuilder(sb).reverse().toString();
/** NOTE !!!
*
* How we get the `lexicographically smaller` one:
* currentStr.compareTo(smallest) < 0
*/
if (smallest.equals("") || currentStr.compareTo(smallest) < 0) {
smallest = currentStr;
}
}
// 3. Standard DFS
dfs(node.left, sb);
dfs(node.right, sb);
/** NOTE !!!
*
* For StringBuilder (NOT a primitive type),
* we MUST do BACKTRACK (undo)
*
* Reason:
* - StringBuilder is a reference type (object)
* - All recursive calls share the SAME StringBuilder instance
* - After exploring subtrees, we must remove the current char
* to restore sb to its state before this call
* - Without this, the path would keep growing incorrectly
*
* Memory model:
*
* dfs(node=5, sb="e")
* ├── sb.append('a') → sb="ea"
* ├── dfs(left, sb="ea")
* │ └── sb.append('b') → sb="eab"
* │ └── sb.deleteCharAt() → sb="ea" ← backtrack!
* ├── dfs(right, sb="ea") ← sb is correctly "ea", not "eab"
* │ └── ...
* └── sb.deleteCharAt() → sb="e" ← backtrack to parent state
*/
// 4. BACKTRACK: Remove the last character before returning to parent
sb.deleteCharAt(sb.length() - 1);
}
Summary Table — primitives vs references in recursion
| State Type | Example | Backtrack? | Reason |
|---|---|---|---|
| Primitive param | int curSum |
No | Each call gets own copy |
| Wrapper param (autoboxed) | Integer curSum |
No | Autoboxing creates new object |
| Local variable | int newSum = curSum + val |
No | Belongs to current stack frame only |
| Instance/global variable | this.curSum |
Yes | Shared across all calls |
| Collection param | List<Integer> path |
Yes | Reference, mutated in-place |
| Array param | int[] path |
Yes | Reference, mutated in-place |
| StringBuilder param | StringBuilder sb |
Yes | Reference, mutated in-place via append()/deleteCharAt() |
Interview Tips
Q: "Do I need to backtrack this variable?"
Decision tree:
1. Is it a primitive (int, long, boolean, char...)?
→ Passed as value → NO backtrack needed
2. Is it a local variable inside the current stack frame?
→ NOT shared → NO backtrack needed
3. Is it an instance/class variable?
→ Shared → YES, must backtrack
4. Is it a collection or array passed as parameter?
→ Reference = shared → YES, must backtrack
Quick mental model:
"If I change this variable, will the CALLER see the change?"
YES → backtrack required
NO → no backtrack needed
Recursion parameter passing
// LC 104
// https://github.com/yennanliu/CS_basics/blob/master/leetcode_java/src/main/java/LeetCodeJava/Recursion/MaximumDepthOfBinaryTree.java
Important Concept: In Java, primitives are passed by value (creates copies).
// ❌ WRONG: Primitive parameter won't persist changes across recursive calls
public int wrongDepth(TreeNode root, int depth) {
if (root == null) return depth;
depth++; // This increment is lost after recursion returns
return Math.max(wrongDepth(root.left, depth), wrongDepth(root.right, depth));
}
// ✅ CORRECT: Use instance variables for state that needs to persist
class Solution {
private int maxDepth = 0; // Instance variable persists across calls
public int maxDepth(TreeNode root) {
depthHelper(root, 0);
return maxDepth;
}
private void depthHelper(TreeNode root, int currentDepth) {
if (root == null) return;
maxDepth = Math.max(maxDepth, currentDepth + 1); // Update global state
depthHelper(root.left, currentDepth + 1);
depthHelper(root.right, currentDepth + 1);
}
}
// ✅ ALTERNATIVE: Return and combine values (functional approach)
public int maxDepth(TreeNode root) {
if (root == null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
Key Takeaway: When you need to track state across recursive calls, either use instance variables or design the recursion to return and combine values.
Passing a value out of a method — the mutable-holder pattern
Core Concept: Reference types (StringBuilder, List, int[], Map, etc.) are passed by reference, not by value. Changes made inside a function persist after the function returns.
Pattern 1: StringBuilder for Path/String Building (LC 694)
// LC 694 - Number of Distinct Islands
// Pattern: Create placeholder → Pass to DFS → Use modified result
Set<String> uniqueIslands = new HashSet<>();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 1) {
// Step 1: Create empty StringBuilder
StringBuilder pathSignature = new StringBuilder();
// Step 2: Pass to DFS — it will modify pathSignature in place
/** NOTE !!!
* We pass `pathSignature` as a reference.
* DFS will call pathSignature.append(...)
* These changes PERSIST after DFS returns
*/
dfs(grid, r, c, pathSignature, 'S');
// Step 3: After DFS returns, pathSignature is populated
if (pathSignature.length() > 0) {
uniqueIslands.add(pathSignature.toString());
}
}
}
}
private void dfs(int[][] grid, int r, int c, StringBuilder path, char direction) {
// Base case
if (r < 0 || r >= rows || c < 0 || c >= cols || grid[r][c] == 0) {
return;
}
grid[r][c] = 0;
// ✅ MODIFY the passed reference
// This directly modifies the caller's StringBuilder object
path.append(direction);
// Explore neighbors
dfs(grid, r + 1, c, path, 'D');
dfs(grid, r - 1, c, path, 'U');
dfs(grid, r, c + 1, path, 'R');
dfs(grid, r, c - 1, path, 'L');
// Backtrack: undo the append
path.append('O');
}
Memory Model:
Main thread:
pathSignature = StringBuilder{} at memory address 0x1000
Call dfs(..., pathSignature, 'S')
├── path parameter = reference to 0x1000
├── path.append('S') → modifies object at 0x1000 → "S"
│
├── Call dfs(..., path, 'D')
│ ├── path parameter = reference to 0x1000 (SAME object!)
│ ├── path.append('D') → modifies object at 0x1000 → "SD"
│ ├── path.append('O') → modifies object at 0x1000 → "SDO"
│ └── return
│
├── path.append('O') → modifies object at 0x1000 → "SDOO"
└── return
Back in main:
pathSignature = StringBuilder{"SDOO"} ✅ (MODIFIED!)
Pattern 2: List for Collecting Results (LC 113 Path Sum II)
// LC 113 - Path Sum II
// Similar pattern but with List<Integer> for path collection
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
dfs(root, targetSum, path, result);
return result;
}
private void dfs(TreeNode node, int remain, List<Integer> path, List<List<Integer>> result) {
if (node == null) return;
// Modify the passed List
path.add(node.val);
if (node.left == null && node.right == null && remain == node.val) {
// Snapshot the path before backtracking
result.add(new ArrayList<>(path));
} else {
dfs(node.left, remain - node.val, path, result);
dfs(node.right, remain - node.val, path, result);
}
// Backtrack: undo the add
path.remove(path.size() - 1);
}
Key Difference from Primitives:
// ❌ PRIMITIVE: Changes don't persist
private void addToSum(int currentSum) {
currentSum += 5; // Only affects local copy
}
int mySum = 10;
addToSum(mySum);
System.out.println(mySum); // Still 10, NOT 15!
// ✅ REFERENCE: Changes persist
private void addToList(List<Integer> list) {
list.add(5); // Affects the original list object
}
List<Integer> myList = new ArrayList<>();
addToList(myList);
System.out.println(myList); // [5] ✅ MODIFIED!
Pattern 3: General Pattern — Create, Pass, Modify, Use
// Generic template for this pattern:
public Type method() {
// 1. Create placeholder (reference type)
SomeRefType placeholder = new SomeRefType();
// 2. Pass to helper function
helperFunction(placeholder, otherParams);
// 3. Use modified result
return placeholder; // or use directly
}
private void helperFunction(SomeRefType data, OtherParams...) {
// Modify the reference — changes persist in caller
data.modify(...);
// Recurse if needed
helperFunction(data, newParams);
// Undo if backtracking required
data.undo(...);
}
Common Reference Types for This Pattern
| Type | Modification Methods | Backtrack Required? | Use Case |
|---|---|---|---|
StringBuilder |
append(x), setCharAt(i, c), deleteCharAt(i) |
✅ Yes | String building with backtracking |
List<T> |
add(x), remove(i), set(i, x) |
✅ Yes | Path/result collection |
int[] / char[] |
arr[i] = value |
✅ Yes | Array modification |
Map<K,V> |
put(k, v), remove(k) |
✅ Yes | Frequency tracking |
Queue<T> |
add(x), poll(), offer(x) |
✅ Maybe | BFS level-by-level |
Set<T> |
add(x), remove(x) |
✅ Yes | Visited tracking |
Primitive int, long |
N/A (pass-by-value) | ❌ No | Only for return or instance vars |
String |
N/A (immutable) | ❌ No | Use StringBuilder instead |
When to Backtrack:
Rule: If the parameter is a reference type that gets MODIFIED, you must UNDO the modification.
Path/List building: path.add(val) → must do path.remove(...)
StringBuilder: sb.append(...) → must do sb.deleteCharAt(...)
Array modification: arr[i] = val → must do arr[i] = oldVal
Map/Set: data.add(x) → must do data.remove(x)
Primitives: No backtrack needed (they're copied)
Pattern 4: List Collection (LC 131 Palindrome Partitioning)
// LC 131
public List<List<String>> partition(String s) {
/**
* NOTE: we can init result, pass it to method,
* modify it, and return as ans
*/
List<List<String>> result = new ArrayList<>();
dfs(0, result, new ArrayList<String>(), s);
return result;
}
private void dfs(int start, List<List<String>> result, List<String> currentList, String s) {
// Base case: reached end of string
if (start == s.length()) {
// Snapshot: add a copy of currentList (don't add reference)
result.add(new ArrayList<>(currentList));
return;
}
// Try all palindromes starting from 'start'
for (int end = start; end < s.length(); end++) {
if (isPalindrome(s, start, end)) {
// Add to current partition
currentList.add(s.substring(start, end + 1));
// Recurse
dfs(end + 1, result, currentList, s);
// Backtrack: remove what we added
currentList.remove(currentList.size() - 1);
}
}
}
private boolean isPalindrome(String s, int l, int r) {
while (l < r) {
if (s.charAt(l) != s.charAt(r)) return false;
l++;
r--;
}
return true;
}
Critical Distinction:
// ❌ WRONG: Adds reference to currentList (not a snapshot)
result.add(currentList); // All entries point to same list!
// ✅ CORRECT: Add a copy
result.add(new ArrayList<>(currentList)); // Each entry is independent
Primitive types are passed by value
// java
// LC 695
/**
* NOTE !!!!
*
* DON'T pass `area` as parameter to the DFS func (getBiggestArea)
*
* Reason:
*
* 1) in java, primitives pass by value.
* `int` is one of the primitives
*
* 2) meaning when we pass `area` to dfs,
* it actually sent as a new COPY everytime,
* which makes us CAN'T track/persist the new area value
*/
// ...
private int _getArea(int[][] grid, boolean[][] seen, int x, int y){
// ...
return 1 + _getArea(grid, seen, x+1, y) +
_getArea(grid, seen, x-1, y) +
_getArea(grid, seen, x, y+1) +
_getArea(grid, seen, x, y-1);
}
Modifying a custom class’s fields directly (v.field -= 1) Priority 1 of 5 — Nice to have
Core Question: When can you write
v.cnt -= 1and when can’t you?
When you CAN modify directly
All three conditions must hold:
- Field is accessible (not
private, or you are inside the class) - Field is not
final - Reference is not
null
class ValCnt {
char val;
int cnt; // package-private, non-final
ValCnt(char val, int cnt) { this.val = val; this.cnt = cnt; }
}
ValCnt v = new ValCnt('a', 3);
v.cnt -= 1; // ✅ allowed: accessible + non-final + non-null
When you CANNOT modify directly
Case 1 — private field (outside the class)
class ValCnt {
private int cnt; // private!
}
v.cnt -= 1; // ❌ compile error — use a setter/method instead
Case 2 — final field
class ValCnt {
final int cnt;
}
v.cnt -= 1; // ❌ compile error — cannot assign to final variable
Case 3 — null reference
ValCnt v = null;
v.cnt -= 1; // ❌ NullPointerException at runtime
Common misconception: final reference vs final field
final ValCnt v = new ValCnt('a', 3);
v.cnt -= 1; // ✅ allowed — final only locks the REFERENCE
v = new ValCnt('b', 1); // ❌ compile error — cannot reassign final reference
final on the variable means you cannot point v to a different object.
It does not prevent mutating the object’s fields.
Integer (wrapper) field — works but autoboxes
class Holder { Integer cnt; }
Holder h = new Holder();
h.cnt = 3;
h.cnt -= 1; // ✅ works, but really: h.cnt = Integer.valueOf(h.cnt.intValue() - 1)
// creates a new Integer object; fails if field is final
Summary table — mutating a custom class in place
| Situation | v.cnt -= 1 allowed? |
|---|---|
int cnt (package-private) |
✅ yes |
private int cnt (outside class) |
❌ compile error |
final int cnt |
❌ compile error |
final ValCnt v (reference is final) |
✅ yes — field still mutable |
v == null |
❌ NullPointerException |
Integer cnt (wrapper) |
✅ works, autoboxes to new object |
Interview trap: v.cnt-- vs --v.cnt vs v.cnt -= 1
All three decrement cnt by 1. The difference is the returned expression value:
v.cnt = 3;
int a = v.cnt--; // a = 3 (returns BEFORE decrement), v.cnt = 2
int b = --v.cnt; // b = 1 (returns AFTER decrement), v.cnt = 1
v.cnt -= 1; // no return value used, v.cnt = 0
Use v.cnt -= 1 when you only care about the side-effect, not the return value.
Assignment copies the reference, not the object
public ListNode reverseList_0_0_1(ListNode head) {
// edge
if(head == null || head.next == null) {
return head;
}
ListNode _prev = null;
/**
* NOTE !!!
*
* we CAN'T just assign `_prev` init val to `res`
* and return `res` as result
* e.g. this is WRONG: return res;
*
* Reason:
* - At this point, res is just a reference to null.
* - As you update _prev during the loop,
* res DOES NOT magically follow _prev.
* It stays stuck at the value it was assigned
* when you created it → null.
*
* So by the end:
* - _prev points to the new head of the reversed list ✅
* - res is still null ❌
*
*
* -> Java references don’t “track” each other after assignment.
* res = _prev copies the reference value `at that moment`
* If _prev later changes, res won’t update.
*
*/
ListNode res = _prev;
while(head != null){
ListNode _next = head.next;
head.next = _prev;
//_prev.next = head;
_prev = head;
head = _next;
}
//return res;
return _prev;
}
Re-constructing nodes instead of mutating them
public Node connect_0_1(Node root) {
if (root == null)
return null;
Queue<Node> q = new LinkedList<>();
q.add(root);
while (!q.isEmpty()) {
int size = q.size();
/**
* NOTE !!!!
*
* via `prev` node,
* we can easily re-connect left - right node in each layer
*
* pattern as below:
*
* Node prev = null;
* for(int i = 0; i < size; i++){
* TreeNode cur = q.poll();
* // NOTE !!!! this
* if(prev == null){
* prev.next = cur;
* }
* // NOTE !!!! this
* prev = cur;
*
* // ....
* }
*
*/
Node prev = null; // prev tracks previous node in this level
for (int i = 0; i < size; i++) {
Node cur = q.poll();
if (prev != null) {
prev.next = cur; // link previous node to current
}
prev = cur;
if (cur.left != null)
q.add(cur.left);
if (cur.right != null)
q.add(cur.right);
}
// last node in level should point to null
if (prev != null)
prev.next = null;
}
return root;
}
Getting a defensive copy of the current instance
// java
// LC 46
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
//...
ans.add(new ArrayList<>(cur));
//...
Integer Math & Operators
ceil vs floor — definition
| Operation | Meaning | Example |
|---|---|---|
Math.ceil(x) |
Round up to nearest integer | ceil(7.0/3) → 3.0 |
Math.floor(x) |
Round down to nearest integer | floor(7.0/3) → 2.0 |
(int)(a/b) |
Truncate toward zero (same as floor for positives) | 7/3 → 2 |
// Using double division + Math.ceil / floor
System.out.println(Math.ceil(7.0 / 3)); // 3.0 (rounds UP)
System.out.println(Math.floor(7.0 / 3)); // 2.0 (rounds DOWN)
System.out.println(7 / 3); // 2 (integer truncation = floor for positives)
// Cast result back to int
int ceilVal = (int) Math.ceil((double) 7 / 3); // 3
int floorVal = (int) Math.floor((double) 7 / 3); // 2
Key gotcha: integer division in Java always truncates toward zero (= floor for positive numbers).
7 / 3 → 2 // floor (positive)
-7 / 3 → -2 // truncates toward zero (NOT floor, which would be -3)
Integer ceiling division — no double needed Priority 1 of 5 — Nice to have
Formula: ceil(a / b) using only integers:
int ceilDiv = (a + b - 1) / b;
Why it works:
ceil(a / b) = (a + b - 1) / b (integer division, b > 0)
Example: a=7, b=3
(7 + 3 - 1) / 3 = 9 / 3 = 3 ✅ (Math.ceil(7.0/3) = 3)
Example: a=6, b=3 (exact division)
(6 + 3 - 1) / 3 = 8 / 3 = 2 ✅ (Math.ceil(6.0/3) = 2)
Example: a=1, b=5
(1 + 5 - 1) / 5 = 5 / 5 = 1 ✅ (Math.ceil(1.0/5) = 1)
Comparison: two ways to compute ceiling division
// Method 1: Integer trick (faster, no casting)
int ceil1 = (val + d - 1) / d;
// Method 2: double cast + Math.ceil (clearer intent, slightly slower)
int ceil2 = (int) Math.ceil((double) val / d);
// Both produce identical results for positive val and d
Classic usage — LC 1283 Find the Smallest Divisor Given a Threshold:
// ceil(val / divisor) without using double
private boolean canDivide(int[] nums, int threshold, int d) {
int sum = 0;
for (int val : nums) {
sum += (val + d - 1) / d; // ← integer ceiling division
if (sum > threshold) return false; // early exit
}
return sum <= threshold;
}
Integer floor division
For positive integers, / already gives floor:
int floorDiv = a / b; // works when a >= 0 and b > 0
For negative numbers, use Math.floorDiv:
Math.floorDiv(-7, 3); // -3 (true floor)
-7 / 3; // -2 (truncation, NOT floor!)
Math.floorDiv(7, 3); // 2 (same as 7/3 for positives)
Ceiling / floor quick reference
| Goal | Code | Notes |
|---|---|---|
| Ceil (double) | (int) Math.ceil((double) a / b) |
Readable, casting required |
| Ceil (integer trick) | (a + b - 1) / b |
Fast, no casting, positive only |
| Floor (positive) | a / b |
Integer division truncates |
| Floor (any sign) | Math.floorDiv(a, b) |
Handles negatives correctly |
| Round (half-up) | (int) Math.round((double) a / b) |
Nearest integer |
| Mid without overflow | l + (r - l) / 2 |
Standard binary search midpoint |
Classic LC problems using ceiling division
| LC | Problem | Ceiling Division Usage |
|---|---|---|
| 1283 | Find the Smallest Divisor Given a Threshold | (val + d - 1) / d per element |
| 1011 | Capacity To Ship Packages Within D Days | (wt + cap - 1) / cap days needed |
| 875 | Koko Eating Bananas | (pile + k - 1) / k hours per pile |
| 2064 | Minimized Maximum of Products Distributed | (n + m - 1) / m ceiling per group |
Pattern: These are all binary search on the answer problems where the check function requires ceiling division to count “how many X fit in Y”.
// General template for this binary search pattern
// (Binary search on divisor/capacity/speed)
int l = 1, r = MAX_VAL;
while (l <= r) {
int mid = l + (r - l) / 2;
if (check(nums, threshold, mid)) {
r = mid - 1; // valid, try smaller
} else {
l = mid + 1; // too small, try larger
}
}
return l;
// Inside check(): use ceiling division
private boolean check(int[] nums, int threshold, int d) {
int sum = 0;
for (int val : nums) {
sum += (val + d - 1) / d; // ceil(val / d)
}
return sum <= threshold;
}
Use long to avoid int overflow
// java
// LC 98
// ...
/**
* NOTE !!!
*
* Use long to handle edge cases for Integer.MIN_VALUE and Integer.MAX_VALUE
* -> use long to handle overflow issue (NOT use int type)
*/
long smallest_val = Long.MIN_VALUE;
long biggest_val = Long.MAX_VALUE;
return check_(root, smallest_val, biggest_val);
// ...
Max and min of three numbers
// java
// LC 152
max = Math.max(Math.max(max * nums[i], min * nums[i]), nums[i]);
min = Math.min(Math.min(temp * nums[i], min * nums[i]), nums[i]);
Counting set bits in an integer
// java
/**
* Integer.bitCount
*
* -> java default get number of "1" from binary representation of a 10 based integer
*
* -> e.g.
* Integer.bitCount(0) = 0
* Integer.bitCount(1) = 1
* Integer.bitCount(2) = 1
* Integer.bitCount(3) = 2
*
* Ref
* - https://blog.csdn.net/weixin_42092787/article/details/106607426
*/
// LC 338
Pre-calculating perfect squares up to N
Trick: Pre-compute all perfect squares ≤ N into a list, then iterate over the list instead of recalculating
i * ieach time. Useful in BFS/DP problems like LC 279 (Perfect Squares).
// Pre-calculate perfect squares up to n
List<Integer> squares = new ArrayList<>();
for (int i = 1; i * i <= n; i++) {
squares.add(i * i);
}
// Usage: iterate over pre-computed squares
for (int square : squares) {
int nextVal = remaining - square;
if (nextVal < 0) break; // squares are sorted, early termination
// ...
}
Random integers with random.nextInt
// java
// LC 528
/** bound : range of random int can be returned */
// @param bound the upper bound (exclusive). Must be positive.
Random random = new Random();
// * @param bound the upper bound (exclusive). Must be positive.
System.out.println(random.nextInt(10));
System.out.println(random.nextInt(10));
System.out.println(random.nextInt(100));
k++ vs ++k
// java
// LC 78
/** NOTE HERE !!!
*
* ++i : i+1 first, then do op
* i++ : do op first, then i+1
*
*/
Quick Reference
Most common patterns
Data Structure Initialization
// Arrays
int[] arr = new int[n]; // Fixed size
int[][] matrix = new int[rows][cols]; // 2D array
// Collections
List<Integer> list = new ArrayList<>(); // Dynamic list
Map<String, Integer> map = new HashMap<>(); // Key-value store
Set<Integer> set = new HashSet<>(); // Unique elements
Queue<Integer> queue = new LinkedList<>(); // FIFO operations
// Priority Queues
PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // Min-heap
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder()); // Max-heap
Essential Conversions
// String ↔ Character Array
String str = "hello";
char[] chars = str.toCharArray(); // String to char array
String newStr = new String(chars); // Char array to String
// Array ↔ List
Integer[] arr = {1, 2, 3};
List<Integer> list = new ArrayList<>(Arrays.asList(arr)); // Array to List
Integer[] newArr = list.toArray(new Integer[0]); // List to Array
// Character to Index (for a-z)
int index = character - 'a'; // 'a'→0, 'b'→1, ..., 'z'→25
Common Operations
// HashMap with default values
map.getOrDefault(key, defaultValue);
map.putIfAbsent(key, value);
// Sorting
Arrays.sort(array); // In-place array sort
Collections.sort(list); // In-place list sort
list.sort(Collections.reverseOrder()); // Reverse order
// String operations
s.charAt(i); // Get character at index
s.substring(start, end); // [start, end) substring
StringBuilder sb = new StringBuilder(); // Mutable string
Performance tips
| Operation | Efficient Approach | Avoid |
|---|---|---|
| String Building | StringBuilder |
String concatenation in loops |
| Character Access | toCharArray() then iterate |
charAt() in tight loops |
| Sorting | Arrays.sort(), Collections.sort() |
Stream sorting for large data |
| Array Printing | Arrays.toString(), Arrays.deepToString() |
Manual iteration |
| Character Mapping | char - 'a' |
indexOf() repeated calls |
Common LeetCode patterns
Frequency Counting
Map<Character, Integer> freq = new HashMap<>();
for (char c : s.toCharArray()) {
freq.put(c, freq.getOrDefault(c, 0) + 1);
}
Two Pointers with Character Comparison
int left = 0, right = s.length() - 1;
while (left < right) {
if (s.charAt(left) != s.charAt(right)) return false;
left++;
right--;
}
Priority Queue for Top-K Problems
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int num : nums) {
heap.offer(num);
if (heap.size() > k) heap.poll();
}
Memory management
- Primitive arrays: More memory efficient than object arrays
- ArrayList: Automatically resizes, initial capacity matters for large datasets
- StringBuilder: Use for string concatenation in loops
- Character arrays: More efficient than String manipulation for character processing