What We're Solving & Order Variations
Generating all distinct orderings of a set of elements is a fundamental problem in combinatorial computing. The Permutations problem asks us to construct every possible arrangement or sequence of a collection of distinct integers, nums.
A permutation represents a specific linear layout of elements. For a set of size n, the total count of unique arrangements is exactly n! (n factorial). For instance, given the array nums = [1, 2, 3], the solution comprises 3! = 6 unique lists:
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]].
O(n!). Using recursive backtracking alongside a lookup buffer to check visited elements is the standard, most efficient way to solve this.
To build intuition for how this search works, imagine laying out physical letter tiles:
- Available Tiles: You have three unique letter tiles on your desk: A, B, and C. You want to write down every possible 3-letter word you can spell using each tile exactly once.
- Making Choices: You start with an empty spelling tray. For the first slot, you have 3 choices: A, B, or C. If you choose A, you place it in the tray. For the second slot, only B and C are left. You choose B. For the third slot, only C is left, completing the word
"ABC". You note this down. - Backtracking: To check other words starting with A, you pick up C, then pick B (this is the backtracking step), and try placing C in the second slot instead, eventually spelling
"ACB".
The Strategy
Backtracking with Used-State Pruning (O(n * n!) Time, O(n) Space)
Let's look at the recursive pointer mechanics of this O(n * n!) runtime algorithm:
- Path Building: We construct a single candidate path list (
path) recursively, adding numbers one-by-one. - Base Case: When the length of
pathmatchesnums.length, we have successfully formed a complete arrangement. We create a copy of the path and add it to our final results list. - State Checking: At each step, we iterate through all elements in
nums. To ensure we do not reuse elements, we maintain a boolean arrayused, whereused[i]tracks whethernums[i]is currently active in our path. This lookup is a constant-timeO(1)operation, which is significantly faster than usingpath.contains(num)(which runs in linearO(n)time). - Backtrack Phase: If the current element is unused, we mark it as used (
used[i] = true), append it topath, and recurse forward. Once that search branch completes, we revert our changes by removing the element from the end ofpathand marking it as unused (used[i] = false). This restores the correct state for neighboring search branches.
Detailed Trace Walkthrough
Let's trace this backtracking search step-by-step on nums = [1, 2, 3]:
- Step 1 (Root Call): We execute
backtrack(path=[], used=[false, false, false]).- Index
i = 0: Value1is unused. We setused[0] = true, updatepath = [1], and recurse.
- Index
- Step 2 (Recurse Level 1): We execute
backtrack(path=[1], used=[true, false, false]).- Index
i = 0: Already used, so we skip it. - Index
i = 1: Value2is unused. We setused[1] = true, updatepath = [1, 2], and recurse.
- Index
- Step 3 (Recurse Level 2): We execute
backtrack(path=[1, 2], used=[true, true, false]).- Index
i = 0,1: Already used, so we skip them. - Index
i = 2: Value3is unused. We setused[2] = true, updatepath = [1, 2, 3], and recurse.
- Index
- Step 4 (Base Case): We execute
backtrack(path=[1, 2, 3], ...).- Since
path.size() == 3, we save a copy of the path. Results:[[1, 2, 3]]. We return to the caller.
- Since
- Step 5 (Backtrack from Level 2):
- We set
used[2] = falseand pop3frompath→path = [1, 2]. The loop ends, and we return.
- We set
- Step 6 (Backtrack from Level 1):
- We set
used[1] = falseand pop2frompath→path = [1]. - The loop continues to index
i = 2. Value3is unused. We setused[2] = true, updatepath = [1, 3], and recurse. - This path will eventually complete to
[1, 3, 2], save it, and backtrack.
- We set
- Step 7 (Exploring Other Roots):
- The algorithm backtracks all the way to the root and starts the next iterations with values
2and3, discovering the remaining permutations.
- The algorithm backtracks all the way to the root and starts the next iterations with values
Code Highlights & Explanations
Key highlights of our Java implementation:
boolean[] usedserves as a fast state-checking buffer to avoid expensive list lookups.result.add(new ArrayList<>(path))makes a deep copy of the path, ensuring that subsequent backtrack modifications do not mutate previously recorded solutions.
Full Code Solution
Below is the complete, self-contained Java source code demonstrating this backtracking strategy, along with a main method for printing the generated combinations.
package io.practise.dsa;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Permutations {
// Backtracking Search: Time O(N * N!), Space O(N)
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums == null || nums.length == 0) {
return result;
}
boolean[] used = new boolean[nums.length];
backtrack(nums, new ArrayList<>(), used, result);
return result;
}
private void backtrack(int[] nums, List<Integer> path, boolean[] used, List<List<Integer>> result) {
// Base case: if current path contains all numbers, a permutation is complete
if (path.size() == nums.length) {
result.add(new ArrayList<>(path)); // Must deep copy
return;
}
for (int i = 0; i < nums.length; i++) {
// Skip elements that are already in our current path
if (used[i]) {
continue;
}
// 1. Choose: Mark as used and add to path
used[i] = true;
path.add(nums[i]);
// 2. Explore: Recurse to generate subsequent positions
backtrack(nums, path, used, result);
// 3. Unchoose: Backtrack by removing and marking unused
path.remove(path.size() - 1);
used[i] = false;
}
}
public static void main(String[] args) {
Permutations solver = new Permutations();
int[] nums = {1, 2, 3};
System.out.println("--- Permutations Demonstration ---");
System.out.println("Input Array: " + Arrays.toString(nums));
List<List<Integer>> list = solver.permute(nums);
System.out.println("All Permutations: " + list);
}
}
Conclusion & Takeaways
Generating permutations requires exploring all possible orderings of a set. By utilizing backtracking and optimizing lookup operations with a used state array, we navigate the state space tree with maximum execution speed and minimum memory overhead.