Introduction & Problem Explanation
The Permutations problem asks us to generate all possible permutations (different
arrangements or orderings) of an array of distinct integers, nums.
A permutation is an arrangement of all members of a set into some sequence or linear order.
For a set of size n, the total number of unique permutations is exactly n! (n
factorial). For instance, if nums = [1, 2, 3], the output contains 3! = 6
combinations:
[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]].
Generating all permutations requires us to explore all branches of a state space tree of size
O(n!). Backtracking combined with a fast lookup buffer (like a boolean array or a set) is the
standard method for this task.
Imagine you have three letter tiles on a desk: A, B, and C. You want to write down all possible 3-letter combinations you can make using each tile exactly once. You start with an empty spelling tray. For the first slot, you have 3 choices: A, B, or C. If you choose A, it goes in the tray. For the second slot, you only have tiles B and C left. If you choose B, you place it down. For the third slot, you only have tile C left, which completes the word "ABC". You write it down. To find other combinations starting with A, you pick up C, then pick up B (backtrack), and place C in the second slot instead, eventually completing "ACB". Repeating this systematic choice and retrieval explores every anagram possible.
The Algorithmic Approach
Backtracking Search with Used Buffer (O(n * n!) Time, O(n) Space)
We build our permutations recursively, building a single permutation list path of length
n step-by-step.
- Base Case: If the size of
pathequalsnums.length, we have formed a complete permutation. We make a copy ofpathand add it to our results list. - Recursive Exploration: We loop through all numbers in
nums. For each numbernum:- If the number is already used in our current path, we skip it to prevent reusing elements. To do this
efficiently, we can use a boolean array
usedwhereused[i]tracks whethernums[i]is currently in our path. This lookup isO(1), which is much faster than callingpath.contains(num), which takesO(n). - Otherwise, we mark the number as used, append it to
path, and recurse forward. - When the recursive call returns, we backtrack by removing the number from the end of
pathand marking it as unused.
- If the number is already used in our current path, we skip it to prevent reusing elements. To do this
efficiently, we can use a boolean array
O(n) due to the recursion stack and the state arrays.
Step-by-Step Execution Walkthrough
Let's trace the backtracking flow for nums = [1, 2, 3]:
- Step 1 (Root Call):
backtrack(path=[], used=[false, false, false]).- Iteration
i = 0:nums[0] (1)is unused. Markused[0] = true, path is[1]. Recurse!
- Iteration
- Step 2 (Recurse Level 1):
backtrack(path=[1], used=[true, false, false]).- Iteration
i = 0: Already used. Skip. - Iteration
i = 1:nums[1] (2)is unused. Markused[1] = true, path is[1, 2]. Recurse!
- Iteration
- Step 3 (Recurse Level 2):
backtrack(path=[1, 2], used=[true, true, false]).- Iteration
i = 0, 1: Already used. Skip. - Iteration
i = 2:nums[2] (3)is unused. Markused[2] = true, path is[1, 2, 3]. Recurse!
- Iteration
- Step 4 (Base Case reached):
backtrack(path=[1, 2, 3]).path.size() == 3. Save copy to result. Return.
- Step 5 (Backtrack from level 2): In Step 3, we unmark
used[2] = false, pop 3. Path becomes[1, 2]. Loop ends. Return. - Step 6 (Backtrack from level 1): In Step 2, we unmark
used[1] = false, pop 2. Path becomes[1].- Loop in Step 2 continues to
i = 2.nums[2] (3)is unused. Markused[2] = true, path becomes[1, 3]. Recurse! - This path will eventually complete to
[1, 3, 2], save it, and backtrack.
- Loop in Step 2 continues to
- Step 7 (Explore other roots): The algorithm backtracks to the root and initiates scans starting with 2 and 3, exploring all paths.
Key Code Snippets & Explanations
Here is why the main logic in the solution is important:
boolean[] used: A high-performance lookup state. Using a boolean array reduces lookup time toO(1), optimizing the overall runtime.if (used[i]) continue;: The pruning check. Skips characters that are already active in the current path.used[i] = true; path.add(nums[i]); ... used[i] = false; path.remove(path.size() - 1);: The classic choose-explore-unchoose cycle, restoring state for adjacent branches.
Java Implementation Code
Below is the complete, self-contained Java source code that solves this problem. It also includes a
main method that traces the execution with console outputs.
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
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.