Data Structures & Algorithms Patterns for Java Backend Engineers: LeetCode Approach
Most Java backend engineers can write a Spring Boot service in their sleep, but freeze when asked to reverse a linked list or find the longest substring without repeating characters. This guide maps the 12 essential DSA problem-solving patterns to real Java idioms — PriorityQueue, ArrayDeque, HashMap — and gives you a repeatable LeetCode study system that fits around a full-time engineering job.
Software Engineer · Java · Spring Boot · Algorithms
Why DSA Matters for Java Backend Engineers in 2026
Google, Meta, Amazon, and every FAANG-adjacent company still use LeetCode-style technical screens — even for senior backend roles. Knowing why a TreeMap costs O(log n) per lookup instead of O(1) (HashMap) makes you a better API designer, not just a better interviewee.
The backend world runs on Java. But most Java DSA guides use Python. This guide translates every pattern into idiomatic Java — with the right collection types, comparator lambdas, and edge-case handling you would use in production code.
The 85% Coverage Rule
Research across thousands of FAANG interview reports shows that 85% of coding questions come from just 12 patterns. Master these patterns and you can solve ~80% of Medium problems on sight:
- Sliding Window
- Two Pointers
- Fast & Slow Pointers
- Merge Intervals
- Cyclic Sort
- In-Place Linked List Reversal
- BFS (Tree & Graph)
- DFS (Tree & Graph)
- Two Heaps
- Subsets / Backtracking
- Binary Search Variants
- Dynamic Programming
Pattern 1: Sliding Window
Use when the problem asks for a contiguous subarray/substring meeting a condition (max sum, longest without repeating chars, minimum window substring).
Java idiom: HashMap<Character, Integer> for frequency counting, two int pointers (left, right).
// LC 3: Longest Substring Without Repeating Characters — O(n) time, O(min(m,n)) space
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> charIndex = new HashMap<>();
int maxLen = 0;
int left = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (charIndex.containsKey(c)) {
// Move left past the last seen position of this char
left = Math.max(left, charIndex.get(c) + 1);
}
charIndex.put(c, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
// LC 76: Minimum Window Substring — O(n) time, O(52) space
public String minWindow(String s, String t) {
int[] freq = new int[128];
for (char c : t.toCharArray()) freq[c]++;
int required = t.length(), left = 0, minLen = Integer.MAX_VALUE, minLeft = 0;
for (int right = 0; right < s.length(); right++) {
if (freq[s.charAt(right)]-- > 0) required--;
while (required == 0) {
if (right - left + 1 < minLen) { minLen = right - left + 1; minLeft = left; }
if (freq[s.charAt(left++)]++ == 0) required++;
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(minLeft, minLeft + minLen);
}
Pattern 2: Two Pointers
Sort the array first, then use a left and right pointer converging toward the middle. Handles 3Sum, container-with-most-water, remove duplicates.
// LC 15: 3Sum — O(n²) time, O(1) extra space
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // skip duplicates
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++; right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
Pattern 3: Fast & Slow Pointers (Floyd's Cycle)
Used for cycle detection in linked lists and arrays. The slow pointer moves one step; the fast pointer moves two steps. When they meet, a cycle exists.
// LC 142: Linked List Cycle II — O(n) time, O(1) space
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
// Cycle detected — find entry point
ListNode entry = head;
while (entry != slow) {
entry = entry.next;
slow = slow.next;
}
return entry;
}
}
return null; // no cycle
}
// LC 287: Find the Duplicate Number — O(n) time, O(1) space (treat array as linked list)
public int findDuplicate(int[] nums) {
int slow = nums[0], fast = nums[0];
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
slow = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
Pattern 4: Merge Intervals
Sort intervals by start time, then linearly merge overlapping ones. Key Java idiom: Arrays.sort(intervals, (a,b) -> a[0] - b[0]).
// LC 56: Merge Intervals — O(n log n) time, O(n) space
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
List<int[]> merged = new ArrayList<>();
for (int[] interval : intervals) {
if (merged.isEmpty() || merged.get(merged.size() - 1)[1] < interval[0]) {
merged.add(interval);
} else {
merged.get(merged.size() - 1)[1] =
Math.max(merged.get(merged.size() - 1)[1], interval[1]);
}
}
return merged.toArray(new int[0][]);
}
// LC 253: Meeting Rooms II (min rooms needed) — O(n log n) time
public int minMeetingRooms(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
PriorityQueue<Integer> endTimes = new PriorityQueue<>();
for (int[] interval : intervals) {
if (!endTimes.isEmpty() && endTimes.peek() <= interval[0]) {
endTimes.poll(); // reuse the room
}
endTimes.offer(interval[1]);
}
return endTimes.size(); // rooms in use
}
Pattern 5: BFS — Breadth-First Search
Level-by-level graph/tree traversal using a Queue (always ArrayDeque in Java, never LinkedList for performance). Perfect for shortest path, level-order traversal, and minimum steps problems.
// LC 102: Binary Tree Level Order Traversal — O(n) time, O(n) space
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) return result;
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(level);
}
return result;
}
// LC 127: Word Ladder — O(M² × N) time where M=word length, N=wordList size
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
Set<String> wordSet = new HashSet<>(wordList);
if (!wordSet.contains(endWord)) return 0;
Deque<String> queue = new ArrayDeque<>();
queue.offer(beginWord);
int steps = 1;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
char[] word = queue.poll().toCharArray();
for (int j = 0; j < word.length; j++) {
char original = word[j];
for (char c = 'a'; c <= 'z'; c++) {
word[j] = c;
String next = new String(word);
if (next.equals(endWord)) return steps + 1;
if (wordSet.remove(next)) queue.offer(next);
}
word[j] = original;
}
}
steps++;
}
return 0;
}
Pattern 6: DFS — Depth-First Search
Use recursion (implicit stack) or an explicit ArrayDeque for iterative DFS. Key for path problems, island counting, and graph coloring.
// LC 200: Number of Islands — O(M×N) time, O(M×N) space (recursion stack)
public int numIslands(char[][] grid) {
int count = 0;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == '1') {
count++;
dfs(grid, r, c);
}
}
}
return count;
}
private void dfs(char[][] grid, int r, int c) {
if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length || grid[r][c] != '1') return;
grid[r][c] = '0'; // mark visited
dfs(grid, r + 1, c);
dfs(grid, r - 1, c);
dfs(grid, r, c + 1);
dfs(grid, r, c - 1);
}
Pattern 7: Two Heaps
Maintain a max-heap for the lower half and a min-heap for the upper half to find the running median in O(log n) per insertion.
// LC 295: Find Median from Data Stream — O(log n) add, O(1) findMedian
class MedianFinder {
private final PriorityQueue<Integer> lower = new PriorityQueue<>(Collections.reverseOrder()); // max-heap
private final PriorityQueue<Integer> upper = new PriorityQueue<>(); // min-heap
public void addNum(int num) {
lower.offer(num);
upper.offer(lower.poll()); // balance: move max of lower to upper
if (lower.size() < upper.size()) {
lower.offer(upper.poll()); // keep lower >= upper in size
}
}
public double findMedian() {
return lower.size() > upper.size()
? lower.peek()
: (lower.peek() + upper.peek()) / 2.0;
}
}
Pattern 8: Backtracking
Explore all possibilities by building a candidate solution step by step, pruning branches that cannot lead to a valid solution.
// LC 46: Permutations — O(n × n!) time, O(n) space (recursion stack)
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, new ArrayList<>(), new boolean[nums.length], result);
return result;
}
private void backtrack(int[] nums, List<Integer> current, boolean[] used, List<List<Integer>> result) {
if (current.size() == nums.length) {
result.add(new ArrayList<>(current));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
current.add(nums[i]);
backtrack(nums, current, used, result);
current.remove(current.size() - 1); // undo
used[i] = false;
}
}
// LC 39: Combination Sum — O(N^(T/M)) where T=target, M=min candidate
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates);
combinationDFS(candidates, 0, target, new ArrayList<>(), result);
return result;
}
private void combinationDFS(int[] candidates, int start, int remaining,
List<Integer> current, List<List<Integer>> result) {
if (remaining == 0) { result.add(new ArrayList<>(current)); return; }
for (int i = start; i < candidates.length; i++) {
if (candidates[i] > remaining) break; // sorted — prune
current.add(candidates[i]);
combinationDFS(candidates, i, remaining - candidates[i], current, result);
current.remove(current.size() - 1);
}
}
Pattern 9: Binary Search Variants
Beyond the basic sorted-array search, binary search applies to any monotonic decision function: "can I achieve X with a budget of K?" Always ask yourself — can I binary search on the answer?
// Template — binary search on answer space
// LC 875: Koko Eating Bananas — O(n log W) time where W = max pile
public int minEatingSpeed(int[] piles, int h) {
int lo = 1, hi = Arrays.stream(piles).max().getAsInt();
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (canFinish(piles, mid, h)) {
hi = mid; // try smaller speed
} else {
lo = mid + 1;
}
}
return lo;
}
private boolean canFinish(int[] piles, int speed, int h) {
int hours = 0;
for (int pile : piles) hours += (pile + speed - 1) / speed; // ceiling division
return hours <= h;
}
// LC 33: Search in Rotated Sorted Array — O(log n) time
public int search(int[] nums, int target) {
int lo = 0, hi = nums.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[lo] <= nums[mid]) { // left half is sorted
if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
else lo = mid + 1;
} else { // right half is sorted
if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
else hi = mid - 1;
}
}
return -1;
}
Pattern 10: Dynamic Programming
Break the problem into overlapping subproblems. Java tip: prefer bottom-up tabulation over top-down memoization for cache-friendly access and avoiding stack overflow on large inputs.
// LC 322: Coin Change — O(amount × coins) time, O(amount) space
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // sentinel "infinity"
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
// LC 1143: Longest Common Subsequence — O(m×n) time, O(m×n) space
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
Pattern 11: Union Find (Disjoint Set Union)
Group elements into disjoint sets. Supports near-O(1) find and union with path compression + union by rank. Used for connected components and Kruskal's MST.
// Reusable Union-Find — O(α(n)) per operation
class UnionFind {
private final int[] parent, rank;
UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
if (parent[x] != x) parent[x] = find(parent[x]); // path compression
return parent[x];
}
boolean union(int x, int y) {
int px = find(x), py = find(y);
if (px == py) return false; // already connected
if (rank[px] < rank[py]) { int t = px; px = py; py = t; }
parent[py] = px;
if (rank[px] == rank[py]) rank[px]++;
return true;
}
}
// LC 200: Number of Islands using Union Find
public int numIslandsUF(char[][] grid) {
int m = grid.length, n = grid[0].length;
UnionFind uf = new UnionFind(m * n);
int islands = 0;
int[][] dirs = {{1,0},{0,1}};
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (grid[r][c] == '1') {
islands++;
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr < m && nc < n && grid[nr][nc] == '1') {
if (uf.union(r * n + c, nr * n + nc)) islands--;
}
}
}
}
}
return islands;
}
Pattern 12: Monotonic Stack
Maintain a stack where elements are always increasing or always decreasing. Solves "next greater element", "largest rectangle in histogram", and "trapping rain water" in O(n).
// LC 84: Largest Rectangle in Histogram — O(n) time, O(n) space
public int largestRectangleArea(int[] heights) {
Deque<Integer> stack = new ArrayDeque<>(); // monotonic increasing stack (indices)
int maxArea = 0;
int n = heights.length;
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i];
while (!stack.isEmpty() && heights[stack.peek()] > h) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
// LC 496: Next Greater Element I — O(n) using monotonic stack
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map<Integer, Integer> map = new HashMap<>(); // num -> next greater
Deque<Integer> stack = new ArrayDeque<>(); // decreasing stack
for (int num : nums2) {
while (!stack.isEmpty() && stack.peek() < num) {
map.put(stack.pop(), num);
}
stack.push(num);
}
int[] result = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
result[i] = map.getOrDefault(nums1[i], -1);
}
return result;
}
Java-Specific Tips That Cost People Interviews
Always Use ArrayDeque, Not LinkedList or Stack
// BAD — Stack is legacy (synchronized, slower)
Stack<Integer> stack = new Stack<>();
// BAD — LinkedList has worse cache locality
Queue<Integer> queue = new LinkedList<>();
// GOOD — ArrayDeque is backed by a resizable array; no synchronization overhead
Deque<Integer> stack = new ArrayDeque<>();
Deque<Integer> queue = new ArrayDeque<>();
PriorityQueue with Custom Comparators
// Min-heap (default)
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
// Max-heap
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// Sort 2D array by second element descending
PriorityQueue<int[]> pqDesc = new PriorityQueue<>((a, b) -> b[1] - a[1]);
Integer Overflow in Comparators
// BAD — integer overflow when a[0] - b[0] overflows int
Arrays.sort(arr, (a, b) -> a[0] - b[0]); // risky if values near Integer.MIN/MAX_VALUE
// GOOD — use Integer.compare() to avoid overflow
Arrays.sort(arr, (a, b) -> Integer.compare(a[0], b[0]));
LeetCode Study System for Busy Engineers
| Week | Patterns | Target Problems | Daily Time |
|---|---|---|---|
| 1–2 | Sliding Window, Two Pointers, Fast & Slow | LC 3, 76, 15, 141, 142, 287 | 45 min |
| 3–4 | BFS, DFS, Merge Intervals | LC 102, 127, 200, 207, 56, 253 | 45 min |
| 5–6 | Binary Search, Two Heaps, Union Find | LC 33, 875, 295, 684, 323 | 45 min |
| 7–8 | DP, Backtracking, Monotonic Stack | LC 322, 1143, 46, 39, 84, 496 | 60 min |
| 9–12 | Mixed mock interviews + Hard problems | 1 timed mock / week | 60 min |
Quick Reference: Java Collections for Each Pattern
| Pattern | Primary Java Structure | Auxiliary |
|---|---|---|
| Sliding Window | int[] / HashMap<Char,Int> |
Two int pointers |
| Two Pointers | int[] (sorted) |
int left, right |
| Fast & Slow | ListNode |
Two node references |
| BFS | Deque<T> (ArrayDeque) |
Set<T> visited |
| DFS | Recursion stack | boolean[] visited |
| Two Heaps | PriorityQueue (min + max) |
Collections.reverseOrder() |
| Backtracking | List<List<T>> |
boolean[] used |
| Binary Search | int[] / long[] |
int lo, hi, mid |
| DP (1D) | int[] dp |
Arrays.fill() |
| DP (2D) | int[][] dp |
Row-space compression |
| Union Find | int[] parent, rank |
Path compression |
| Monotonic Stack | Deque<Integer> (ArrayDeque) |
Index-based for width calc |
Conclusion
Mastering DSA as a Java backend engineer is not about memorizing solutions — it's about recognizing the shape of a problem and mapping it to the right pattern. A sliding-window sub-array question is always solved with two pointers and a HashMap. A "find connected components" question is always Union Find or DFS. A "minimum steps" question is always BFS.
Build your pattern library one at a time, validate it with 3–5 LeetCode problems per pattern, and run one timed mock per week. In 12 weeks you will be in the top 15% of Java interviewees — not because you are smarter, but because you have a system.
Leave a Comment
Related Posts
Software Engineer · Java · Spring Boot · Algorithms