Permutations Backtracking — 48GB Heap Blowup at N=12
12 factorial produces 479 million permutations needing 48GB, exhausting 8GB heap.
20+ years shipping performance-critical code where algorithms decide the bill. Notes here come from systems that actually shipped.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- Backtracking builds permutations by fixing one element at a time, recursing into the rest, then undoing the choice — the undo step is not optional
- Two main implementations: visited-array (boolean[]) gives lexicographic output; in-place swap rearranges the input array and produces a different ordering
- Time complexity: O(N × N!) — N! leaves in the recursion tree, O(N) copy work at each leaf
- Memory grows to O(N × N!) if you store all results; use a Consumer callback to keep memory O(N) regardless of how many permutations are generated
- The number one bug: storing the list reference instead of a copy at the base case — all results become empty lists when backtracking finishes
- For duplicate inputs, sort first then skip a candidate if it equals the previous element AND that previous element is not currently in the active path — both conditions together are required
Permutations backtracking is a brute-force algorithmic strategy for generating all possible orderings of a set of elements. It systematically explores a decision tree where each level chooses one remaining element, recurses, then 'un-chooses' it (backtracks) to try the next option.
The core problem it solves is exhaustive enumeration — you need every arrangement, not just a count. At N=12, the factorial explosion (479 million permutations) turns this into a memory and performance minefield: a naive implementation that copies lists at each recursion can allocate 48GB of heap because it holds every intermediate permutation simultaneously, not just the final results.
This is the classic 'heap blowup' that catches engineers off guard when they scale from small N to production workloads.
In the ecosystem, permutations backtracking competes with iterative algorithms like Heap's algorithm (O(N!) time, O(1) extra space) and next_permutation-based generation (lexicographic order, no recursion). You'd use backtracking when you need to prune the search space — for example, filtering permutations that violate constraints (like in Sudoku or N-Queens) — because you can check validity mid-recursion and skip entire branches.
Don't use it for pure enumeration of small sets (N ≤ 8) where simpler iterative methods suffice, or for N > 12 where the runtime becomes impractical regardless of memory. Real-world tools like Python's itertools.permutations use a C-optimized iterative approach that avoids recursion overhead entirely.
The two dominant implementations are the visited-array approach (tracks used elements with a boolean array, copies partial results) and in-place swap backtracking (swaps elements in the original array, recurses, then swaps back). The visited array is easier to reason about and naturally handles duplicates with a sorted input and a 'skip if same as previous and previous not used' check, but it allocates O(N) extra space per recursion level and copies lists, causing heap blowup at N=12.
In-place swap uses O(1) extra space and avoids copies, but it's trickier to debug (mutating state during recursion) and requires sorting and a 'skip swap if same element already placed at this position' rule for duplicates. The performance cliff is stark: at N=12, visited-array can exhaust 48GB heap in seconds, while in-place swap stays under 1GB — but both still take minutes to run.
For production, you'd never generate all permutations in memory; instead, use a consumer pattern (yield or callback) to stream results to disk or process them incrementally.
Imagine you are arranging three friends — Alice, Bob, and Carol — in a line for a photo. You try every possible order: ABC, ACB, BAC, BCA, CAB, CBA. Backtracking is exactly this trial-and-error process made systematic: you place one person, then try all options for the next spot, and when you hit a dead end (everyone is placed), you step back and swap in someone else. The backtrack is literally the moment you say 'nope, let us undo that and try the next option.' The crucial thing people miss: every time you backtrack, you restore the arrangement exactly to what it was before the last choice. Without that restoration, the next attempt starts from corrupted state and produces garbage. That restoration step is the entire secret of why backtracking works.
Permutations show up everywhere in computing — from scheduling jobs on a processor, to cracking password combinations, to solving the Travelling Salesman Problem where you test every possible route. Any time you need to explore every possible ordering of a set of items, you are in permutation territory. The naive approach of nested loops collapses instantly once your input grows beyond three or four items, which is exactly why a systematic recursive strategy is non-negotiable.
Backtracking solves this elegantly by building each permutation one element at a time, making a choice, recursing into it, and then undoing that choice to explore the next branch. It is a depth-first traversal of a decision tree where each level represents filling one position and each branch represents which element goes there. The magic is the undo step — without it, you corrupt state between branches and get garbage results. Backtracking keeps state clean by design.
By the end of this article you will be able to implement two distinct backtracking strategies for permutations — visited-array and in-place swap — understand exactly what happens on the call stack at each recursive depth, handle duplicate input elements correctly, identify the performance cliffs and memory costs that bring down production systems, and answer the follow-up questions interviewers use to separate candidates who memorised the solution from those who genuinely understand it.
Why Permutations Backtracking Explodes Your Heap
Permutations backtracking is a brute-force algorithm that enumerates all possible orderings of a set by recursively swapping or selecting elements, pruning branches that cannot yield a valid permutation. At each recursion depth, it places one element and recurses on the remainder, producing n! leaves. For N=12, that's 479 million leaf nodes — each carrying a partial or full copy of the state. The core mechanic is depth-first exploration with state mutation and undo (backtrack), but naive implementations clone the entire array at every call, turning O(n!) time into O(n n!) memory. In practice, the heap blowup is not theoretical: a Java program generating permutations of 12 distinct elements with ArrayList copying at each recursion step will allocate ~48 GB of intermediate objects before the JVM throws OutOfMemoryError. The fix is in-place swapping with a single mutable array and backtracking by swapping back — reducing memory from O(n n!) to O(n). This pattern matters because real systems (scheduling, constraint solvers, test case generators) hit this wall at surprisingly small input sizes when developers treat permutations as a toy problem.
The Recursive Decision Tree — How Backtracking Builds Every Permutation
Before touching code, you need a mental model of what the algorithm is actually doing. Think of it as a tree. At the root you have an empty arrangement and all N elements available. At depth 1 you pick the first element — N choices. At depth 2 you pick the second from the remaining N-1 elements. At depth 3 from the remaining N-2. The leaves of this tree, all at depth N, are the complete permutations. There are exactly N factorial of them.
For N=3 with elements [1, 2, 3], the tree looks like this:
[] / | [1] [2] [3] / \ / \ / [1,2] [1,3] [2,1] [2,3] [3,1] [3,2] | | | | | | [1,2,3][1,3,2][2,1,3][2,3,1][3,1,2][3,2,1]
Each internal node represents a partial permutation. Each leaf is a complete permutation. The total number of nodes is N! plus (N-1)! plus ... plus 1!, which for N=3 is 6 plus 2 plus 1 equals 9 nodes visited. That structure is what the recursion is walking.
The critical insight: you do not build this tree in memory all at once. You walk it depth-first, keeping only the current path from root to the current node in memory. When you reach a leaf, you record that permutation. When you backtrack up, you restore the path to what it was before descending — that restoration is the undo step, and it is why only one path is live in memory at any moment.
Why is the time complexity O(N times N!) rather than just O(N!)? Because at each of the N! leaves, you do O(N) work to copy the completed permutation into the result. The internal node visits add a lower-order term. The dominant cost is those N! copies of length N.
package io.thecodeforge.algorithms; import java.util.ArrayList; import java.util.List; /** * Approach 1: Visited-Array Backtracking * * Uses a boolean[] to track which elements are already placed * in the current partial permutation. At each recursive call, * we iterate over all elements, skip used ones, place the next * available element, recurse, then UNDO the placement. * * Output order: lexicographic when input is sorted. * * Time: O(N * N!) — N! permutations, O(N) copy at each leaf * Space: O(N) — recursion depth + visited array + current path * (O(N * N!) if accumulating all results) */ public class PermutationDecisionTree { public static List<List<Integer>> generatePermutations(int[] inputElements) { List<List<Integer>> allPermutations = new ArrayList<>(); boolean[] isElementUsed = new boolean[inputElements.length]; List<Integer> currentPermutation = new ArrayList<>(); backtrack(inputElements, isElementUsed, currentPermutation, allPermutations); return allPermutations; } private static void backtrack(\n int[] inputElements,\n boolean[] isElementUsed,\n List<Integer> currentPermutation,\n List<List<Integer>> allPermutations) { // BASE CASE: current permutation has all N elements if (currentPermutation.size() == inputElements.length) { // CRITICAL: copy the list, not the reference. // currentPermutation is mutated throughout backtracking. // Storing the reference means every entry in allPermutations // points to the same object — which ends up empty. allPermutations.add(new ArrayList<>(currentPermutation)); return; } for (int index = 0; index < inputElements.length; index++) { if (isElementUsed[index]) { continue; // skip elements already in the current path } // --- CHOOSE --- isElementUsed[index] = true; currentPermutation.add(inputElements[index]); // --- EXPLORE --- backtrack(inputElements, isElementUsed, currentPermutation, allPermutations); // --- UNCHOOSE (backtrack) --- // Restore state exactly as it was before this iteration. // The next iteration of the loop must see a clean slate. currentPermutation.remove(currentPermutation.size() - 1); isElementUsed[index] = false; } } public static void main(String[] args) { int[] elements = {1, 2, 3}; List<List<Integer>> result = generatePermutations(elements); System.out.println("Total permutations: " + result.size()); // 3! = 6 result.forEach(System.out::println); // Demonstrate the reference trap explicitly System.out.println("\n--- Reference Trap Demo ---"); List<List<Integer>> trapResult = new ArrayList<>(); boolean[] used = new boolean[elements.length]; List<Integer> path = new ArrayList<>(); // Uncomment the line below and comment the copy line in backtrack to see all empty lists: // trapResult.add(path) instead of new ArrayList<>(path) System.out.println("Always use new ArrayList<>(currentPermutation) at the base case."); } }
In-Place Swap Backtracking — Faster, Lower Memory, Trickier to Reason About
The visited-array approach is clean and easy to reason about, but it carries overhead: a boolean array, a separate ArrayList for the current path, and an O(N) list copy at every leaf. The in-place swap approach eliminates the boolean array entirely and works directly on the input array — the foundation that Heap's algorithm builds on.
The idea is to treat the array as two regions: a fixed prefix (indices 0 to start minus 1) and a remaining suffix (indices start to end). At each recursive call, you try swapping each element in the remaining region into position start, recurse with start plus 1, then swap back to restore the array for the next iteration. When start equals the array length, the entire array is one complete permutation — snapshot it and return.
The output order is not lexicographic. For [1, 2, 3], the in-place swap produces [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,2,1], [3,1,2] — note the last two entries differ from the visited-array output. This is documented in the comparison table and must be acknowledged in any API that cares about ordering.
The trade-off that matters in practice: the swap-back step is subtle and easy to forget. If you omit it, you silently corrupt future branches. No exception is thrown. The output is just wrong — some permutations are duplicated, others are missing. Without a test suite that checks all N! results against a known set, this bug can survive code review and reach production.
package io.thecodeforge.algorithms; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * Approach 2: In-Place Swap Backtracking * * Works directly on the input array. No visited[] needed. * The fixed prefix grows by one per recursive level. * At each level, swap each remaining element into the current * position, recurse, then swap BACK to restore array state. * * Output order: NOT lexicographic — differs from visited-array approach. * Document this in any API where ordering matters. * * Time: O(N * N!) — same asymptotic as visited-array * Space: O(N) — only the recursion stack (no extra path list or boolean array) * plus O(N) per leaf for the array snapshot */ public class InPlaceSwapPermutation { public static List<int[]> generatePermutations(int[] inputArray) { List<int[]> allPermutations = new ArrayList<>(); // Work on a copy — never mutate the caller's array int[] workingArray = Arrays.copyOf(inputArray, inputArray.length); swapBacktrack(workingArray, 0, allPermutations); return allPermutations; } private static void swapBacktrack( int[] workingArray, int startIndex, List<int[]> allPermutations) { // BASE CASE: startIndex reached the end — full array is one permutation if (startIndex == workingArray.length) { // Arrays are mutable — must copy, not store the reference allPermutations.add(Arrays.copyOf(workingArray, workingArray.length)); return; } for (int candidate = startIndex; candidate < workingArray.length; candidate++) { // --- CHOOSE: bring candidate into the fixed prefix --- swap(workingArray, startIndex, candidate); // --- EXPLORE: fix startIndex, recurse on the rest --- swapBacktrack(workingArray, startIndex + 1, allPermutations); // --- UNCHOOSE: restore to pre-swap state --- // Without this, subsequent loop iterations see a corrupted array. // This is the most commonly forgotten line in interview code. swap(workingArray, startIndex, candidate); } } private static void swap(int[] array, int i, int j) {\n int temp = array[i];\n array[i] = array[j];\n array[j] = temp;\n } public static void main(String[] args) { int[] elements = {1, 2, 3}; List<int[]> result = generatePermutations(elements); System.out.println("Total permutations: " + result.size()); // 3! = 6 System.out.println("Note: order is NOT lexicographic (differs from visited-array approach)"); for (int[] permutation : result) { System.out.println(Arrays.toString(permutation)); } // Verify the original array is unmodified System.out.println("Original array after call: " + Arrays.toString(elements)); } }
Advantages and Disadvantages: Visited Array vs In-Place Swap
When choosing between the visited-array and in-place swap approaches for generating permutations, it's essential to understand the trade-offs in terms of memory, output order, code clarity, and performance. The table below summarizes the key differences.
| Criterion | Visited Array (boolean[]) | In-Place Swap |
|---|---|---|
| Memory overhead | O(N) for visited array + O(N) for path list | O(N) recursion stack only (no extra arrays) |
| Output order | Lexicographic when input sorted | Non-lexicographic (different order) |
| Ease of debugging | Clear choose-explore-unchoose steps; easy to trace | Swap-back step is subtle and often forgotten |
| Duplicate handling | Sort + prune using !used[i-1] | Per-level HashSet to skip duplicate values |
| Performance (N=10) | Baseline | ~1.4x faster (fewer allocations) |
| Code clarity | Very readable, ideal for interviews | More concise but harder to reason about |
| Mutation of input | Input array unchanged (uses indexes) | Modifies a copy of the input array |
| Base case copying | Copy path list (ArrayList) | Copy working array (int[]) |
| Early exit support | Easy to add AtomicBoolean flag | Same method; requires careful restoration |
Visited Array is the recommended choice for interviews and production code where lexicographic order is required and code maintainability is a priority. It's easier to debug and less error-prone.
In-Place Swap is better suited for performance-critical scenarios where memory allocations must be minimized and the caller does not depend on a specific output order. Its speed advantage becomes more pronounced as N grows, but the risk of the missing swap-back bug increases with code complexity.
Choose the approach that matches your constraints: if you need ordered results and have a moderate N (≤10), visited array is safer. If you are processing permutations via a Consumer and want every last nanosecond, in-place swap is the way to go.
Handling Duplicates — When Your Input Has Repeated Elements
Both approaches above assume all input elements are distinct. Feed [1, 1, 2] to either and you get 6 permutations with [1, 1, 2] appearing twice — because the algorithm treats the two 1s as distinct elements. The correct answer is 3 factorial divided by 2 factorial equals 3 unique permutations: [1,1,2], [1,2,1], [2,1,1].
The fix for the visited-array approach requires two things working together. First, sort the input — this groups duplicate values adjacently. Second, add a pruning condition inside the loop: skip element at index i if input[i] equals input[i-1] AND used[i-1] is false.
The NOT condition on used[i-1] is the part that trips people up in interviews. Here is why it is correct. When used[i-1] is false, it means the previous identical element is not currently in our active path — we have already explored, or are about to explore, a branch starting with that element. Starting another branch with an identical element at the same position would generate exactly the same subtree. So we skip it. When used[i-1] is true, the previous identical element IS in our current path at a different position — the resulting permutation will be genuinely different, so we must not skip it.
For the in-place swap approach, duplicate handling is more involved. The standard fix is a HashSet per recursive call level that tracks which values have already been swapped into the startIndex position. Before swapping a candidate in, check if that value has already been placed at this level — if so, skip it. This prevents exploring identical subtrees at each level without requiring a sort.
package io.thecodeforge.algorithms; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Permutations with duplicate input elements — two approaches. * * Approach A (visited-array): sort input, skip adjacent duplicates * with the !used[i-1] pruning condition. * * Approach B (in-place swap): use a per-level HashSet to track * which values have already been placed at the current position. */ public class PermutationWithDuplicates { // --- Approach A: Visited-Array with Sort + Prune --- public static List<List<Integer>> generateUniquePermutations(int[] inputElements) { // Sort is REQUIRED — groups duplicates adjacently so adjacent-skip works Arrays.sort(inputElements); List<List<Integer>> uniquePermutations = new ArrayList<>(); boolean[] isElementUsed = new boolean[inputElements.length]; List<Integer> currentPermutation = new ArrayList<>(); backtrackUnique(inputElements, isElementUsed, currentPermutation, uniquePermutations); return uniquePermutations; } private static void backtrackUnique(\n int[] inputElements,\n boolean[] isElementUsed,\n List<Integer> currentPermutation,\n List<List<Integer>> uniquePermutations) { if (currentPermutation.size() == inputElements.length) { uniquePermutations.add(new ArrayList<>(currentPermutation)); return; } for (int index = 0; index < inputElements.length; index++) { if (isElementUsed[index]) { continue; } // DUPLICATE PRUNING: // Skip if this element equals the previous one AND the previous // one is NOT currently in our active path. // // WHY !used[i-1] and not used[i-1]? // When used[i-1] is false: the previous duplicate is not in our current // path. We already explored (or will explore) a branch starting with // that element. Starting another branch with an identical element at // the same position generates the same subtree — skip it. // // When used[i-1] is true: the previous duplicate IS in our path at a // different position. The resulting permutation is genuinely different // from any already generated — do NOT skip it. if (index > 0 && inputElements[index] == inputElements[index - 1] && !isElementUsed[index - 1]) { continue; } isElementUsed[index] = true; currentPermutation.add(inputElements[index]); backtrackUnique(inputElements, isElementUsed, currentPermutation, uniquePermutations); currentPermutation.remove(currentPermutation.size() - 1); isElementUsed[index] = false; } } // --- Approach B: In-Place Swap with Per-Level Set --- public static List<int[]> generateUniqueSwapPermutations(int[] inputArray) { List<int[]> result = new ArrayList<>(); int[] working = Arrays.copyOf(inputArray, inputArray.length); swapBacktrackUnique(working, 0, result); return result; } private static void swapBacktrackUnique( int[] arr, int startIndex, List<int[]> result) { if (startIndex == arr.length) { result.add(Arrays.copyOf(arr, arr.length)); return; } // Track which VALUES have already been placed at startIndex this call // Using values (not indices) because we care about semantic duplicates Set<Integer> placedAtThisLevel = new HashSet<>(); for (int candidate = startIndex; candidate < arr.length; candidate++) { // Skip if this value has already been the fixed element at this level if (placedAtThisLevel.contains(arr[candidate])) { continue; } placedAtThisLevel.add(arr[candidate]); swap(arr, startIndex, candidate); swapBacktrackUnique(arr, startIndex + 1, result); swap(arr, startIndex, candidate); } } private static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } public static void main(String[] args) { int[] withDuplicates = {1, 1, 2}; System.out.println("=== Approach A: Visited-Array + Sort + Prune ==="); List<List<Integer>> resultA = generateUniquePermutations(withDuplicates.clone()); System.out.println("Unique permutations: " + resultA.size()); // 3!/2! = 3 resultA.forEach(System.out::println); System.out.println("\n=== Approach B: In-Place Swap + Per-Level Set ==="); List<int[]> resultB = generateUniqueSwapPermutations(withDuplicates.clone()); System.out.println("Unique permutations: " + resultB.size()); // also 3 resultB.stream().map(Arrays::toString).forEach(System.out::println); } }
Performance Cliffs, Memory Costs, and the Consumer Pattern
N factorial grows catastrophically fast. At N=10 you have 3,628,800 permutations. At N=12 it is 479,001,600. At N=13 it is over 6 billion. If you are storing all permutations in a List, the memory cost is O(N times N!). For N=12, that is roughly 479 million arrays of 12 integers each — on the JVM with object headers and ArrayList overhead, you are looking at 40 to 50 gigabytes. This is not a production-safe pattern.
The correct approach for large N is a Consumer callback. Pass a Consumer<int[]> into the backtracking function and call it at each leaf instead of accumulating results. This keeps memory at O(N) regardless of N factorial — only the current path, the recursion stack of depth N, and one O(N) snapshot per leaf call are ever live simultaneously.
For N up to 8 or 9, the visited-array approach with ArrayList storage is fine for most applications. For N up to 12 or 13 in performance-critical paths, use the in-place swap approach with a Consumer callback — you eliminate all list allocations during traversal and pay only one O(N) array copy at each leaf. For N beyond 13, you are almost certainly solving the wrong problem. At that scale, you need constraint satisfaction, heuristic search, or a way to generate only the permutations that satisfy your constraints rather than all of them.
Early exit is a common requirement when you only need the first valid permutation. The Consumer pattern does not natively support early exit — the recursion runs to completion. The correct approach is an AtomicBoolean flag passed through the recursive calls, checked at the top of each frame. The alternative of throwing a custom RuntimeException for early exit is a well-known anti-pattern in Java: exceptions are significantly slower than conditional checks, they pollute stack traces, and they violate the semantic contract that exceptions signal unexpected errors. Use the flag.
package io.thecodeforge.algorithms; import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; /** * Memory-efficient permutation streaming using a Consumer callback. * * Memory cost: O(N) regardless of N! — only current path and * recursion stack are live. No result list ever grows. * * Also demonstrates early-exit via AtomicBoolean flag. * Do NOT use exception-throwing for early exit — exceptions are * orders of magnitude slower and pollute stack traces. */ public class PermutationWithConsumer { /** * Stream all permutations to a consumer. Memory stays O(N). * Safe for N up to 12-13. Add input validation for production use. */ public static void streamPermutations(int[] inputArray, Consumer<int[]> onPermutation) { if (inputArray.length > 12) { throw new IllegalArgumentException( "N=" + inputArray.length + " exceeds safe limit. " + "N=12 generates ~479M permutations. Use constraint search instead."); } int[] working = Arrays.copyOf(inputArray, inputArray.length); swapBacktrack(working, 0, onPermutation); } private static void swapBacktrack( int[] working, int startIndex, Consumer<int[]> onPermutation) { if (startIndex == working.length) { onPermutation.accept(Arrays.copyOf(working, working.length)); return; } for (int candidate = startIndex; candidate < working.length; candidate++) { swap(working, startIndex, candidate); swapBacktrack(working, startIndex + 1, onPermutation); swap(working, startIndex, candidate); } } /** * Stream permutations with early exit support. * Returns true if a valid permutation was found, false if all were exhausted. * * The consumer returns true to continue, false to stop. * Uses AtomicBoolean flag — NOT exception throwing. */ public static boolean streamUntilFound(\n int[] inputArray,\n java.util.function.Predicate<int[]> onPermutation) { int[] working = Arrays.copyOf(inputArray, inputArray.length); AtomicBoolean found = new AtomicBoolean(false); swapBacktrackWithExit(working, 0, onPermutation, found); return found.get(); } private static void swapBacktrackWithExit( int[] working, int startIndex, java.util.function.Predicate<int[]> onPermutation, AtomicBoolean stopFlag) {\n\n // Check the flag at every frame entry — stops recursion as early as possible\n if (stopFlag.get()) return;\n\n if (startIndex == working.length) {\n boolean continueSearching = onPermutation.test(\n Arrays.copyOf(working, working.length));\n if (!continueSearching) {\n stopFlag.set(true);\n } return; } for (int candidate = startIndex; candidate < working.length; candidate++) { if (stopFlag.get()) return; // check before each branch swap(working, startIndex, candidate); swapBacktrackWithExit(working, startIndex + 1, onPermutation, stopFlag); swap(working, startIndex, candidate); } } private static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } public static void main(String[] args) { int[] elements = {1, 2, 3, 4}; // 4! = 24 int[] count = {0}; System.out.println("=== Streaming all permutations (memory stays O(N)) ==="); streamPermutations(elements, perm -> count[0]++); System.out.println("Total permutations processed: " + count[0]); // 24 System.out.println("\n=== Early exit: find first permutation starting with 3 ==="); boolean found = streamUntilFound( elements, perm -> {\n if (perm[0] == 3) {\n System.out.println(\"Found: \" + Arrays.toString(perm));\n return false; // stop searching\n }\n return true; // continue\n }\n );\n System.out.println(\"Search completed, found: \" + found);\n\n System.out.println(\"\\n=== Input validation for N > 12 ===\");\n try {\n streamPermutations(new int[13], perm -> {});\n } catch (IllegalArgumentException e) {\n System.out.println(\"Caught: \" + e.getMessage());\n }\n }\n}", "output": "=== Streaming all permutations (memory stays O(N)) ===\nTotal permutations processed: 24\n\n=== Early exit: find first permutation starting with 3 ===\nFound: [3, 1, 2]\nSearch completed, found: true\n\n=== Input validation for N > 12 ===\nCaught: N=13 exceeds safe limit. N=12 generates ~479M permutations. Use constraint search instead." }
Bitmask Backtracking — Cache-Friendly Optimisation for Small N
For small N up to about 20, there is a third implementation worth knowing: replace the boolean array with an integer bitmask. The idea is that each bit in an int (or long) represents whether the corresponding element is currently in the active path. Bit i set to 1 means element i is used; bit i set to 0 means it is available.
Checking if element i is used becomes a single AND operation: (usedMask & (1 << i)) != 0. Marking it used is an OR: usedMask | (1 << i). Clearing it on backtrack is an AND with complement: usedMask & ~(1 << i). All three are single CPU instructions with no array bounds check and no memory indirection. The bitmask lives in a CPU register throughout the recursion.
The result is better cache behaviour and reduced GC pressure. You are not allocating a boolean array per stack frame, and the mask value is passed by value — it is automatically restored when the recursive call returns, which means you do not even need the explicit unchoose step for the mask. The choose step is usedMask | (1 << i), and you pass the new mask down. When the call returns, the caller still has the old mask. This is one of the few backtracking implementations where the undo step is free.
The practical limit: a Java int holds 32 bits, so this works for N up to 31. A long handles N up to 63. For N beyond 63, revert to boolean[]. In practice, if N exceeds 20, you are already in the territory where generating all permutations is not a viable strategy regardless of which approach you use.
package io.thecodeforge.algorithms; import java.util.ArrayList; import java.util.List; /** * Approach 3: Bitmask Backtracking * * Replaces boolean[] with an int bitmask. * Bit i = 1 means element i is in the current path. * Bit i = 0 means element i is available. * * Benefits over boolean[]: * - No array allocation per level * - Single AND/OR/NOT operations — single CPU instructions * - Mask passed by value: undo step is automatic on return * - Better cache locality — mask stays in a register * * Limit: N <= 31 for int, N <= 63 for long. * * Time: O(N * N!) — same asymptotic as other approaches * Space: O(N) — recursion stack only; bitmask is a scalar */ public class BitmaskPermutation { public static List<List<Integer>> generatePermutations(int[] inputElements) { if (inputElements.length > 31) { throw new IllegalArgumentException( "Bitmask approach requires N <= 31. Got N=" + inputElements.length); } List<List<Integer>> result = new ArrayList<>(); List<Integer> path = new ArrayList<>(); // Start with usedMask = 0: no elements in use backtrack(inputElements, 0, path, result); return result; } private static void backtrack(\n int[] arr,\n int usedMask, // passed by value — automatic undo on return\n List<Integer> path,\n List<List<Integer>> result) { if (path.size() == arr.length) { result.add(new ArrayList<>(path)); return; } for (int i = 0; i < arr.length; i++) { // Check if bit i is set — single AND instruction if ((usedMask & (1 << i)) != 0) { continue; // element i is already in the path } // CHOOSE: set bit i — single OR instruction // Pass the new mask down; caller's mask is unchanged on return path.add(arr[i]); backtrack(arr, usedMask | (1 << i), path, result); // UNCHOOSE: remove from path // No mask restoration needed — usedMask was passed by value path.remove(path.size() - 1); } } public static void main(String[] args) { int[] elements = {1, 2, 3}; // 3! = 6 List<List<Integer>> result = generatePermutations(elements); System.out.println("Total permutations: " + result.size()); result.forEach(System.out::println); System.out.println("\n--- Bitmask behaviour walkthrough for N=3 ---"); System.out.println("Initial mask : 000 (no elements used)"); System.out.println("After placing element 0: mask = 001"); System.out.println("After placing element 2: mask = 101"); System.out.println("After placing element 1: mask = 111 (all used — leaf reached)"); System.out.println("On return from leaf: mask reverts to 101 automatically (pass-by-value)"); System.out.println("\n--- Input validation for N > 31 ---"); try { generatePermutations(new int[32]); } catch (IllegalArgumentException e) { System.out.println("Caught: " + e.getMessage()); } } }
The Real Problem: Memory Layout and Why Your Recursion Eats Stack
Every recursive call in backtracking adds a stack frame. That's obvious. What's less obvious is the memory layout cost when you use a visited array vs in-place swap. The visited array allocates O(n) extra heap memory for the boolean flags, plus the recursion depth of O(n) on the call stack. For n=10, that's trivial. For n=12, you're pushing 479 million permutations through the stack. Each frame holds local variables, return addresses, and the visited array reference. Java's default stack size is 1MB. At 12 depth, you're fine. But slap a large input with duplicates and you'll hit StackOverflowError before lunch.
The in-place swap method avoids the visited array heap allocation but still pays the recursion depth tax. The trick is to understand that the stack frames themselves become the bottleneck for large n. If you're dealing with n > 10, switch to an iterative algorithm or use tail recursion optimisation if your language supports it. Java doesn't. So either bump the JVM stack size with -Xss or refactor to a consumer pattern that yields results incrementally without holding all permutations in memory.
// io.thecodeforge — dsa tutorial public class StackOverflowDiagnostic { public static void main(String[] args) { // Simulate deep recursion for n=12 try { permute(new int[12], 0); } catch (StackOverflowError e) { System.err.println("Stack blew at depth: " + e.getMessage()); } } static void permute(int[] arr, int index) { if (index == arr.length) return; for (int i = index; i < arr.length; i++) { swap(arr, index, i); permute(arr, index + 1); swap(arr, index, i); // backtrack } } static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } }
Handling Duplicates the Production Way: Sorting and Skipping
When your input has repeated elements, naive backtracking blows up. You generate the same permutation multiple times because swapping identical values produces no change. The junior fix: use a HashSet to dedupe at output. That works but leaks memory and wastes CPU cycles generating duplicates you'll discard.
The senior fix: sort the input first, then skip duplicate branching at each recursion level. Here's the rule: after swapping, sort the remaining suffix. Wait, that's O(n log n) per call. No. Better: before recursing, check if the current element has already been used at this depth. Use a local boolean array or a HashMap at each recursion level to track what you've already swapped into the current position. That guarantees each branch is unique without post-processing.
In Java, you can maintain a Set<Character> per call to track used elements. But that allocates per frame. Alternative: sort the input once, then in the loop, skip if i > index && arr[i] == arr[i-1] && !visited[i-1]. This works if you use a visited array approach. For in-place swap, you need a separate Set per recursion depth. Trade-off: memory vs time. In production with n <= 10, the set overhead is negligible. For n > 10, you're already in trouble.
// io.thecodeforge — dsa tutorial import java.util.*; public class DuplicateFreePermutations { public static void main(String[] args) { String input = "AAB"; List<String> results = new ArrayList<>(); permuteUnique(input.toCharArray(), 0, results); System.out.println(results); } static void permuteUnique(char[] chars, int index, List<String> results) { if (index == chars.length - 1) { results.add(new String(chars)); return; } Set<Character> used = new HashSet<>(); for (int i = index; i < chars.length; i++) { if (used.contains(chars[i])) continue; used.add(chars[i]); swap(chars, index, i); permuteUnique(chars, index + 1, results); swap(chars, index, i); // backtrack } } static void swap(char[] arr, int i, int j) { char tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } }
Subset & Power Set Pattern — Backtracking's Lighter Sibling
Most devs grind permutations first. That's a mistake. The subset (power set) pattern is where you actually learn backtracking without the heap pressure. Every permutation problem decomposes into nested subset decisions — master this, and permutations become a trivial variation.
The subset pattern asks: for each element, do I include it or skip it? That's it. Two branches per node. Compare that to N! explosion for permutations. The call stack stays shallow — O(N) depth — and the total subsets are 2^N, not N!. Production builds often prefer subsets when order doesn't matter because the memory profile is dramatically kinder.
Here's the core: you maintain a current list, push a decision, recurse, then pop. The trick is to pass the start index to avoid revisiting earlier elements — that's what distinguishes subsets from permutations. If you ever hear a junior say "I'll just generate all permutations and filter," show them this. They'll thank you when their laptop doesn't melt.
// io.thecodeforge — dsa tutorial import java.util.*; public class SubsetsBacktrack { public List<List<Integer>> subsets(int[] nums) { List<List<Integer>> result = new ArrayList<>(); backtrack(nums, 0, new ArrayList<>(), result); return result; } private void backtrack(int[] nums, int start, List<Integer> current, List<List<Integer>> result) { result.add(new ArrayList<>(current)); for (int i = start; i < nums.length; i++) { current.add(nums[i]); backtrack(nums, i + 1, current, result); current.remove(current.size() - 1); } } }
Master Template + Cheat Sheet — One Pattern to Rule All Combinations
After a decade of code reviews, I've seen every permutation, subset, and combination solution. 90% of them follow the same skeleton. Memorize this — it's the only backtracking template you need.
The pattern: a recursion with a for-loop, a start index for combinations/subsets, a visited array or swap logic for permutations, and a prune condition for duplicates (sort + skip). The rest is cosmetic. Every production backtracking algorithm I've written — from e-commerce catalog filtering to constraint satisfaction in scheduling — traces back to this.
Here's the ugly truth: most devs write backtracking from scratch every time. That's cargo cult engineering. Lock down this template, then you only vary the decision logic and base case. Pass the current state as parameters, always clone when adding to results (or you'll get mutability bugs that take hours to debug), and never use global variables unless you enjoy thread-safety nightmares.
// io.thecodeforge — dsa tutorial import java.util.*; public class BacktrackTemplate { public List<List<Integer>> solve(int[] nums) { Arrays.sort(nums); // only for duplicate skipping List<List<Integer>> result = new ArrayList<>(); backtrack(nums, 0, new ArrayList<>(), new boolean[nums.length], result); return result; } private void backtrack(int[] nums, int start, List<Integer> current, boolean[] used, List<List<Integer>> result) { // Base case — add result, then return or continue // Decision: for-loop from start (subsets) or 0 (permutations) for (int i = start; i < nums.length; i++) { if (used[i]) continue; // Duplicate prune if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue; current.add(nums[i]); used[i] = true; backtrack(nums, i + 1, current, used, result); // i+1 for subsets used[i] = false; current.remove(current.size() - 1); } } }
new ArrayList<>(current) when adding to results. One shared reference = silent data corruption.Memory Meltdown: Permutation Generator Crashes Production Scheduler
- Never store all N factorial permutations in memory for N greater than 10. Use streaming or incremental processing with a Consumer callback.
- Always profile memory for combinatorial algorithms before deploying. Theoretical space complexity written as O(N times N!) does not feel large until you substitute N=12 and do the arithmetic.
- If you must store results, limit N to 8 or 9 and document the constraint explicitly in the API contract with the reason why.
- Test with N values that stress the boundary, not just N values that are convenient. N=7 passing does not tell you anything useful about N=12.
System.out.println("At base case, storing: " + currentPermutation);allPermutations.add(new ArrayList<>(currentPermutation));System.out.println("Before swap: " + Arrays.toString(arr));System.out.println("After undo swap: " + Arrays.toString(arr));java -Xmx512m -verbose:gc YourClass — watch GC frequency to confirm memory pressurejmap -histo:live <pid> | head -20 — identify which object type is consuming heapSystem.out.println("Sorted input: " + Arrays.toString(inputElements)); — add this before backtracking beginsAssert result size equals expected: int expected = factorial(inputElements.length) / duplicateDivisor; assert result.size() == expected;| File | Command / Code | Purpose |
|---|---|---|
| io | /** | The Recursive Decision Tree |
| io | /** | In-Place Swap Backtracking |
| io | /** | Handling Duplicates |
| io | /** | Performance Cliffs, Memory Costs, and the Consumer Pattern |
| io | /** | Bitmask Backtracking |
| StackOverflowDiagnostic.java | public class StackOverflowDiagnostic { | The Real Problem |
| DuplicateFreePermutations.java | public class DuplicateFreePermutations { | Handling Duplicates the Production Way |
| SubsetsBacktrack.java | public class SubsetsBacktrack { | Subset & Power Set Pattern |
| BacktrackTemplate.java | public class BacktrackTemplate { | Master Template + Cheat Sheet |
Key takeaways
Common mistakes to avoid
5 patternsStoring the list reference instead of a copy at the base case
Forgetting the swap-back step in in-place swap backtracking
Not sorting input before using adjacent-skip pruning for duplicates
Using exception throwing for early exit from permutation generation
Not validating N before generating all permutations
Practice These on LeetCode
Interview Questions on This Topic
Explain the time and space complexity of generating all permutations using backtracking.
How would you handle duplicate elements in the input when generating permutations?
What are the key differences between the visited-array and in-place swap backtracking approaches?
How would you implement early exit for permutation generation without using exceptions?
When would a bitmask approach be beneficial over a boolean array for backtracking?
Frequently Asked Questions
Because the backtracking algorithm mutates the current permutation list continuously. If you store the list reference at each leaf instead of a copy, all stored references point to the same object, which ends up empty when the algorithm finishes. This produces no compile error or runtime exception — just silently wrong output. The fix is always to copy with new ArrayList<>(currentPermutation) at the base case.
If accumulating all results, N ≤ 8 or 9 is safe depending on heap size. For streaming with a Consumer callback, N ≤ 12 or 13 is practical. Beyond N=13, the number of permutations (6.2 billion for N=13) becomes intractable even for streaming — you should consider constraint satisfaction or heuristic search instead.
The bitmask is passed by value to the recursive call. The caller computes a new mask (usedMask | (1 << i)) and passes it down. When the recursive call returns, the caller's copy of the mask remains unchanged — the bit was never set in the caller's scope. This eliminates the need for an explicit undo step on the mask.
When used[i-1] is false, the previous duplicate is not in the current path, meaning we already explored or will explore a branch starting with that element. Starting another branch with an identical element at the same position generates the same subtree — so we skip it. When used[i-1] is true, the previous duplicate is in the path at a different position, producing a genuinely different permutation — we must not skip it.
You should work on a copy of the input array to avoid mutating the caller's data. The in-place swap approach modifies the working array throughout recursion. Always create a copy using Arrays.copyOf(input, input.length) before starting backtracking.
20+ years shipping performance-critical code where algorithms decide the bill. Notes here come from systems that actually shipped.
That's Greedy & Backtracking. Mark it forged?
11 min read · try the examples if you haven't