Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 65

Permutations with Backtracking and Swaps

Recommended resources

Hello! Let's dive into our next lesson on backtracking.

In our previous session, we mastered generating all subsets using a recursive "include/exclude" pattern. This was about choosing which elements belong in a group, where order didn't matter.

Today, we'll tackle a related but distinct problem: generating all permutations. Here, the goal is to find every possible ordering of a given set of elements. For example, for the set {1, 2, 3}, the permutations are [1, 2, 3], [1, 3, 2], [2, 1, 3], and so on. Unlike subsets, every element must be used exactly once, and their order is what defines a unique permutation.

This is another classic problem that frequently appears in technical interviews. By the end of this lesson, you will be able to generate all permutations of a set using a space-efficient, swap-based backtracking approach.

1. The Core Idea: Fix, Permute, and Swap

The problem of finding all permutations of n elements can be broken down recursively. The main idea is to:

  1. Fix an element for the first position.
  2. Permute the remaining n-1 elements.
  3. Repeat this process by placing every available element into the first position.

How can we do this efficiently? Instead of using extra data structures to keep track of used elements (like a boolean array or a HashSet), we can perform the operation in-place within the array itself using swaps.

The strategy for an array nums at a given index is:

  • Iterate from i = index to the end of the array.
  • In each iteration, swap the element at index with the element at i. This effectively places the i-th element at the index-th position.
  • After the swap, we have "fixed" the element at index. We then make a recursive call to generate all permutations for the rest of the array, starting from index + 1.
  • Once the recursive call returns, we swap back the elements to restore the array to its original state. This is the crucial backtracking step, ensuring that the next iteration of the loop works with a clean slate.

Let's watch a video that provides an excellent visualization of this process.

L13. Print all Permutations of a String/Array | Recursion | Approach - 2

The video 'L13. Print all Permutations of a String/Array' from the 'take U forward' channel demonstrates this swap-based technique with a clear, step-by-step example.

Please watch from 00:34 to 13:30. Pay close attention to how the algorithm builds the recursion tree for the input [1, 2, 3]. Notice how each level of recursion corresponds to fixing an element at a specific position (index), and how the swaps explore all possibilities for that position.

This recursion tree illustrates the process beautifully. At each level, we decide which number gets to be in the current position, and then we recursively solve the problem for the positions that follow.

Recursion Tree for Permutations of "ABC"
A recursion tree for generating permutations of \"ABC\". Each level fixes a character's position. The green part is the fixed prefix, and the red part is what remains to be permuted. Swaps explore the choices for the current position.

2. Implementation in Java: The Backtracking Pattern

Now, let's translate this logic into a Java implementation. The structure will feel familiar after our lesson on subsets, but the core logic inside the loop is different.

To understand the complete logic and the importance of each step, this article provides a clear, annotated code example.

Finding All Permutations Recursively in Java

The article 'Finding All Permutations Recursively in Java' by Alexander Obregon explains the swap-based method and the importance of backtracking.

Please read the sections 'The Core Recursive Idea', 'Why Backtracking Matters', and 'Collecting Results in a List'. Focus on how the swap operation is used before and after the recursive call, and how the final results are collected into a List<List<Integer>>.

Code Deconstruction

Here is a clean implementation based on the resources. This is the pattern you'll want to have ready for interviews.

import java.util.ArrayList;
import java.util.List;
import java.util.Collections; // For Collections.swap if using a List

class Solution {

    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(0, nums, result);
        return result;
    }

    private void backtrack(int index, int[] nums, List<List<Integer>> result) {
        // Base Case: If the index reaches the end, we have a complete permutation.
        if (index == nums.length) {
            // Create a new list and copy the elements from the current state of `nums`.
            List<Integer> currentPermutation = new ArrayList<>();
            for (int num : nums) {
                currentPermutation.add(num);
            }
            result.add(currentPermutation);
            return;
        }

        // Recursive Step: Iterate through the elements from the current index to the end.
        for (int i = index; i < nums.length; i++) {
            // 1. Make a choice: Swap the current element `i` into the `index` position.
            swap(nums, index, i);

            // 2. Recurse: Explore permutations for the rest of the array.
            backtrack(index + 1, nums, result);

            // 3. Backtrack: Undo the choice by swapping back.
            swap(nums, index, i);
        }
    }

    private void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

Key Points to Analyze:

  1. index Parameter: This parameter tracks the current position we are trying to fill. The recursion depth is directly tied to this index.

  2. Base Case: if (index == nums.length): When our index has moved past the last element, it means we have successfully placed an element in every position from 0 to n-1. At this point, the nums array itself holds one complete permutation. We add a copy of it to our results.

  3. The Loop and Swap: The loop for (int i = index; ...) is the engine of our exploration.

    • swap(nums, index, i): This is the action. We are choosing the i-th element to be placed at the index-th position.
    • backtrack(index + 1, ...): This is the recursion. We trust that the function will handle all permutations for the subarray starting at index + 1.
    • swap(nums, index, i) (the second time): This is the backtrack. After returning from the recursion, we undo our swap. This is vital! It ensures that when the loop proceeds to i+1, the array is in the same state as it was before we decided to place the i-th element, allowing us to correctly place the (i+1)-th element at the index position next.
Test your understanding!

You are running permute on nums = [1, 2, 3]. The backtrack function is called with index = 0. The loop starts with i = 0. It swaps nums[0] with nums[0], so the array is still [1, 2, 3]. Then it calls backtrack(1, [1, 2, 3], result).

Inside this new recursive call (index = 1), what will be the first two values of i in its for loop, and what will the state of the nums array be just before the next recursive call in each of those two iterations?

Show answer

The loop runs for (int i = index; ...), so with index = 1, the loop will start with i = 1.

  1. i = 1: swap(nums, 1, 1) is performed. The array remains [1, 2, 3]. The next recursive call will be backtrack(2, [1, 2, 3], result).
  2. i = 2: After the first call returns and backtracks, the array is restored to [1, 2, 3]. The loop continues with i = 2. swap(nums, 1, 2) is performed. The array becomes [1, 3, 2]. The next recursive call will be backtrack(2, [1, 3, 2], result).

3. A Critical Insight: Why does the loop start from index?

A common point of confusion is why the inner loop starts from i = index and not i = 0. If we started from i = 0 every time, we would generate redundant permutations. By starting from index, we ensure we only swap with elements that have not yet been fixed in the prefix 0...index-1.

This next video segment explains this exact point very well. Although the audio is in Hindi, the visual animation and code clearly demonstrate the logic.

Permutations | INTUITIVE | Backtracking Template | Two Ways | Similar Problems | Leetcode 46

The video 'Permutations | INTUITIVE | Backtracking Template' from 'codestorywithMIK' has a great section explaining why the swap loop should start from the current index.

Please watch from 22:18 to 28:00. Focus entirely on the visual animation on the screen. The instructor shows what happens if you try to swap from the beginning of the array (i=0) versus starting from the current index (i=idx). This visualization is key to building the right intuition.


4. Complexity Analysis

For any interview, you need to know the complexity. Given your background, this should be a straightforward analysis.

  • Time Complexity: O(N × N!)

    • How many permutations are there for N distinct elements? N! (N factorial).
    • The recursion tree has N! leaf nodes, and each leaf node corresponds to one complete permutation.
    • At each leaf node, we perform an O(N) operation to copy the current state of the array into a new list to be added to our results.
    • Thus, the total time complexity is dominated by this collection step, resulting in O(N × N!).
  • Space Complexity: O(N) (Auxiliary Space)

    • The primary space used by the algorithm itself (excluding the output list) is the recursion call stack.
    • The maximum depth of the recursion is N. Therefore, the auxiliary space complexity is O(N).
    • If you are asked to include the space for the output, it would be O(N × N!), as we store N! permutations, each of length N. Always clarify this with your interviewer!

You can review this analysis in the take U forward video (LINK) from timestamp 13:30 to 14:30.


Conclusion

Today we've added a powerful and efficient pattern to your backtracking toolkit. The swap-based method for permutations is elegant because it avoids the overhead of extra space for tracking state, modifying the input array directly.

Key Takeaways:

  • Permutations are about arrangement, while subsets are about selection.
  • The core strategy is to fix a position, explore possibilities for it, and recurse on the remainder of the array.
  • The swap(index, i) followed by backtrack(index + 1) and then swap(index, i) again is the fundamental pattern for this problem.
  • Starting the exploration loop from i = index is crucial for avoiding duplicate work and generating correct, unique permutations.
  • Be ready to discuss the O(N × N!) time and O(N) auxiliary space complexity.

Preview of the Next Lesson:
We've covered subsets (order doesn't matter, any size) and permutations (order matters, size N). In the next lesson, we will look at combinations. This can be seen as a hybrid: we will generate all subsets of a specific size k. This will require us to adapt the backtracking logic we learned for subsets to handle the size constraint.