title: Permutations
slug: permutations
difficulty: medium
leetcode_id: 46
leetcode_url: https://leetcode.com/problems/permutations/
categories:
  - arrays
  - recursion
patterns:
  - backtracking

description: |
  Given an array `nums` of distinct integers, return *all the possible permutations*. You can return the answer in **any order**.

constraints: |
  - `1 <= nums.length <= 6`
  - `-10 <= nums[i] <= 10`
  - All the integers of `nums` are **unique**

examples:
  - input: "nums = [1,2,3]"
    output: "[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]"
    explanation: "All 6 permutations of the three distinct integers."
  - input: "nums = [0,1]"
    output: "[[0,1],[1,0]]"
    explanation: "Both orderings of two elements."
  - input: "nums = [1]"
    output: "[[1]]"
    explanation: "A single element has only one permutation."

explanation:
  intuition: |
    Imagine you have a set of numbered cards and want to arrange them in every possible order on a table.

    For each position in the arrangement, you pick one card from the remaining unused cards, place it, then move to the next position. Once all positions are filled, you have one complete permutation. To find *all* permutations, you systematically try every possible choice at each position.

    Think of it like a decision tree: at each level, you branch out by choosing one of the remaining elements. When you reach a leaf (all elements placed), you've found a valid permutation. Then you **backtrack** — undo your last choice and try a different one.

    This is the essence of backtracking: build a solution incrementally, and when you complete one solution or hit a dead end, reverse your last decision to explore other possibilities.

  approach: |
    We solve this using **Backtracking**:

    **Step 1: Set up the recursive function**

    - Create a helper function that takes the current partial permutation being built
    - Maintain a way to track which elements have been used (either via a set, or by swapping elements in-place)

    &nbsp;

    **Step 2: Base case**

    - When the current permutation has the same length as the input array, we've placed all elements
    - Add a copy of the current permutation to our results list

    &nbsp;

    **Step 3: Recursive case — explore all choices**

    - For each element in `nums` that hasn't been used yet:
      - **Choose**: Add the element to the current permutation, mark it as used
      - **Explore**: Recursively call the function to fill the next position
      - **Unchoose (Backtrack)**: Remove the element from the current permutation, mark it as unused

    &nbsp;

    **Step 4: Return all collected permutations**

    - After the recursion completes, the results list contains all valid permutations

    &nbsp;

    The backtracking pattern ensures we systematically explore every possible ordering without repetition.

  common_pitfalls:
    - title: Forgetting to Backtrack
      description: |
        After exploring a branch, you must undo your choice before trying the next option. If you forget to remove the element from the current path or unmark it as used, subsequent branches will have incorrect state.

        For example, if you add `2` to the path and recurse, you must remove `2` after returning — otherwise the next iteration might try to build `[1, 2, 3, 2]` which is invalid.
      wrong_approach: "Only adding elements without removing them after recursion"
      correct_approach: "Add element, recurse, then remove element (backtrack)"

    - title: Storing References Instead of Copies
      description: |
        When you find a valid permutation and add it to results, you must add a **copy** of the current path, not a reference to it.

        If you do `results.append(current_path)` without copying, all entries in results will point to the same list object, which keeps changing as you backtrack. You'll end up with all identical (and likely empty) lists.
      wrong_approach: "results.append(current_path)"
      correct_approach: "results.append(current_path.copy()) or results.append(list(current_path))"

    - title: Not Handling the Used Set Correctly
      description: |
        When using a set to track used elements, make sure to:
        - Add the element to `used` before recursing
        - Remove it from `used` after recursing

        Alternatively, you can use the element's index instead of its value, or use an in-place swap approach to avoid the set entirely.

  key_takeaways:
    - "**Backtracking template**: Choose, explore, unchoose — this pattern applies to permutations, combinations, subsets, and many constraint satisfaction problems"
    - "**Time complexity is inherently factorial**: Generating all permutations of `n` elements requires O(n!) work since there are n! permutations, each of length n"
    - "**In-place swapping alternative**: Instead of tracking used elements, you can swap elements to the 'current position' and recurse on the remaining portion"
    - "**Foundation for harder problems**: Permutations II (with duplicates) builds directly on this, adding skip logic for repeated elements"

  time_complexity: "O(n * n!). There are n! permutations, and we spend O(n) time copying each one to the result."
  space_complexity: "O(n). The recursion stack depth is n, and the current path stores up to n elements. The output list of n! permutations is typically not counted in auxiliary space."

solutions:
  - approach_name: Backtracking with Used Set
    is_optimal: true
    code: |
      def permute(nums: list[int]) -> list[list[int]]:
          results = []

          def backtrack(current_path: list[int], used: set[int]) -> None:
              # Base case: permutation is complete
              if len(current_path) == len(nums):
                  results.append(current_path.copy())  # Must copy!
                  return

              # Try each unused element
              for num in nums:
                  if num in used:
                      continue  # Skip already-used elements

                  # Choose
                  current_path.append(num)
                  used.add(num)

                  # Explore
                  backtrack(current_path, used)

                  # Unchoose (backtrack)
                  current_path.pop()
                  used.remove(num)

          backtrack([], set())
          return results
    explanation: |
      **Time Complexity:** O(n * n!) — We generate all n! permutations, and copying each permutation takes O(n).

      **Space Complexity:** O(n) — The recursion depth is n, plus the current path and used set each hold at most n elements.

      This approach uses a set to track which elements have been included in the current permutation. At each step, we iterate through all elements and skip those already used.

  - approach_name: Backtracking with In-Place Swaps
    is_optimal: true
    code: |
      def permute(nums: list[int]) -> list[list[int]]:
          results = []

          def backtrack(start: int) -> None:
              # Base case: all positions filled
              if start == len(nums):
                  results.append(nums.copy())
                  return

              # Try each element from start to end in the current position
              for i in range(start, len(nums)):
                  # Swap element at i into the start position
                  nums[start], nums[i] = nums[i], nums[start]

                  # Recurse to fill remaining positions
                  backtrack(start + 1)

                  # Swap back to restore original order
                  nums[start], nums[i] = nums[i], nums[start]

          backtrack(0)
          return results
    explanation: |
      **Time Complexity:** O(n * n!) — Same as the first approach.

      **Space Complexity:** O(n) — Only the recursion stack, no additional data structures for tracking used elements.

      This approach avoids the `used` set by swapping elements in place. The portion `nums[0:start]` represents the current partial permutation, and we try placing each element from `nums[start:]` at position `start` by swapping.

  - approach_name: Iterative (Build Permutations Step by Step)
    is_optimal: false
    code: |
      def permute(nums: list[int]) -> list[list[int]]:
          # Start with an empty permutation
          result = [[]]

          for num in nums:
              new_result = []
              # For each existing partial permutation
              for perm in result:
                  # Insert num at every possible position
                  for i in range(len(perm) + 1):
                      new_perm = perm[:i] + [num] + perm[i:]
                      new_result.append(new_perm)
              result = new_result

          return result
    explanation: |
      **Time Complexity:** O(n * n!) — Same overall, though with different constant factors.

      **Space Complexity:** O(n * n!) — We store all intermediate permutations, not just the final ones.

      This iterative approach builds permutations by inserting each new number into all possible positions of existing permutations. While it avoids recursion, it uses more memory because it stores all partial results at each stage.
