Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
239: Backtracking Patterns in Java
Backtracking is essentially a specialized version of recursion where you're searching for a solution by trying one path, realizing it's a dead end, and then "undoing" your last move to try a different one. If you've ever solved a maze by putting your hand on the wall and turning back when you hit a corner, you've performed a manual backtracking algorithm.
The hardest part for most developers isn't the recursion itself—it's managing the state. I often see learners treat backtracking like a standard depth-first search, forgetting that they have to leave the environment exactly how they found it before the function returns. Let's look at a common mess-up.
The Permutation State Leak
Suppose you're trying to generate all possible permutations of a string. You might write something like this. It looks logically sound at first glance: you pick a character, mark it as used, and move to the next position.
public void findPermutations(String str, StringBuilder current, boolean[] used) {
if (current.length() == str.length()) {
System.out.println(current.toString());
return;
}
for (int i = 0; i < str.length(); i++) {
if (!used[i]) {
used[i] = true;
current.append(str.charAt(i));
findPermutations(str, current, used);
// I'm missing something here...
}
}
}
If you run this with the string "ABC", you won't get six permutations. You'll get one correct one ("ABC"), and then the code will either crash or produce gibberish. Why? Because current and used are passed by reference. When the recursion hits the base case and returns, the StringBuilder still contains "ABC" and the used array is still all true. The previous calls in the stack have no idea the state was modified, so they can't try the next available character.
Restoring State After Recursion
To fix this, you need to implement the "Choose, Explore, Unchoose" pattern. This is the heartbeat of every backtracking algorithm. You make a choice, you dive deep into that choice, and then you explicitly revert that choice before the method finishes.
public void findPermutations(String str, StringBuilder current, boolean[] used) {
if (current.length() == str.length()) {
System.out.println(current.toString());
return;
}
for (int i = 0; i < str.length(); i++) {
if (!used[i]) {
// 1. CHOOSE
used[i] = true;
current.append(str.charAt(i));
// 2. EXPLORE
findPermutations(str, current, used);
// 3. UNCHOOSE (The magic step)
used[i] = false;
current.deleteCharAt(current.length() - 1);
}
}
}
By deleting the last character and resetting the used flag, you're cleaning up after yourself. When the loop moves to i + 1, it's as if the previous attempt never happened. I always tell people to think of it like a transactional database: if the "transaction" (the recursive call) finishes, you must roll back the changes to the shared state so the next transaction starts fresh.
Pruning the Search Tree
In more complex problems—like the N-Queens problem or Sudoku solvers—backtracking becomes powerful when you add "pruning." Pruning is just a fancy way of saying "stop searching this path as soon as you know it's impossible."
Instead of waiting until you reach the base case to see if a solution is valid, you check the constraints before making the recursive call. If placing a queen at (row, col) violates a rule, you don't call findPermutations for that branch at all. This turns an exponential nightmare into something manageable. If you find your backtracking code is taking forever to run, you're likely exploring paths that could have been pruned five levels higher up the tree.
📋 Practical Task
Build a Valid Parentheses Generator
Your task is to implement a backtracking algorithm that generates all combinations of well-formed parentheses. Given an integer n representing the number of pairs, you need to return a list of all possible strings.
Constraints:
- You cannot add a closing parenthesis
')'unless there is a matching open parenthesis'('already in the string. - You cannot add more than
nopening parentheses.
Requirements:
- Implement a method
public List<String> generateParenthesis(int n). - Use a
StringBuilderto maintain the current string state. - Ensure you are using the "Choose, Explore, Unchoose" pattern to manage the
StringBuilder. - Verify your logic: For
n = 3, your code should produce["((()))","(()())","(())()","()(())","()()()"].
There are no comments for now.