Menu
DSA interview questionsQuestion 103 of 147

DSA interview question · Question 103 of 147

Permutations: Every Ordering of Distinct Values with Backtracking

  • Medium
  • coding
  • ~15 min
  • High relevance
  • 5 min read
  • Updated Oct 2026

Short answer

Build the ordering one position at a time. At each position try every value not yet used, mark it used, recurse, then unmark it and remove it from the path. When the path is as long as the input, record a copy. There are n! permutations and each copy costs O(n), so the time is O(n * n!), with O(n) extra space for the path, the used flags and the recursion.

On this page
  1. Problem
  2. Examples
  3. Approach 1: recursion by inserting into smaller permutations
  4. Approach 2: optimal, backtracking with a used set
  5. Template
  6. Python solution
  7. Complexity
  8. Tests
  9. Edge cases and pitfalls
  10. Where this shows up in data engineering

Problem

Given a list of distinct integers, return every possible ordering of those integers. Each ordering uses every value exactly once. The order in which you return the orderings does not matter.

This is widely known as LeetCode 46, “Permutations”. Unlike Subsets, where order inside a result does not matter, here [1, 2] and [2, 1] are different answers.

Assume up to 8 values.

Examples

values: [8, 1]
answer: [8, 1], [1, 8]

values: [3, 6, 9]
answer: [3, 6, 9], [3, 9, 6], [6, 3, 9], [6, 9, 3], [9, 3, 6], [9, 6, 3]   (3! = 6)

values: [42]
answer: [42]

Approach 1: recursion by inserting into smaller permutations

The permutations of [a] + rest are obtained by inserting a into every gap of every permutation of rest.

def permutations_insert(nums):
    if not nums:
        return [[]]
    first, rest = nums[0], nums[1:]
    result = []
    for perm in permutations_insert(rest):
        for pos in range(len(perm) + 1):
            result.append(perm[:pos] + [first] + perm[pos:])
    return result

It is correct and also O(n * n!), but it builds many intermediate lists. The backtracking version is what the interviewer usually wants, because it extends to duplicates and to constraints (for example “no two adjacent values may differ by more than 3”).

Approach 2: optimal, backtracking with a used set

Template

backtrack(path):
    if len(path) == n: record a copy of path; return
    for each value v in nums:
        if v is used: continue
        mark v used; path.append(v)        # choose
        backtrack(path)                    # explore
        path.pop(); unmark v               # un-choose

The difference from Subsets is that the loop always starts at 0 (any unused value can go in the next position) and a result is recorded only at full length.

Python solution

def permutations(nums):
    result, path = [], []
    used = [False] * len(nums)

    def backtrack():
        if len(path) == len(nums):
            result.append(path[:])
            return
        for i, value in enumerate(nums):
            if used[i]:
                continue
            used[i] = True
            path.append(value)
            backtrack()
            path.pop()
            used[i] = False

    backtrack()
    return result

An in-place alternative swaps each candidate into position first, recurses on first + 1, then swaps back. It saves the used array:

def permutations_swap(nums):
    nums = list(nums)
    result = []

    def backtrack(first):
        if first == len(nums):
            result.append(nums[:])
            return
        for i in range(first, len(nums)):
            nums[first], nums[i] = nums[i], nums[first]
            backtrack(first + 1)
            nums[first], nums[i] = nums[i], nums[first]

    backtrack(0)
    return result

Complexity

  • Time: O(n * n!). There are n! leaves and copying each result is O(n). The internal nodes add only a constant factor (the sum n!/k! over k is below e * n!).
  • Space: O(n) for the path, the flags and the recursion, excluding the output.

Tests

import itertools

def norm(result):
    return sorted(map(tuple, result))

for fn in (permutations, permutations_swap, permutations_insert):
    assert norm(fn([])) == [()]                       # one empty ordering
    assert norm(fn([42])) == [(42,)]                  # single element
    assert norm(fn([8, 1])) == [(1, 8), (8, 1)]
    assert len(fn([3, 6, 9])) == 6
    assert norm(fn([3, 6, 9, 12])) == sorted(itertools.permutations([3, 6, 9, 12]))
    assert len(set(map(tuple, fn([1, 2, 3, 4, 5])))) == 120   # all distinct

# Input list is not modified
data = [5, 4, 3]
permutations_swap(data)
assert data == [5, 4, 3]

Edge cases and pitfalls

  • Recording path without copying it. All results would end up as the same, finally empty, list.
  • Forgetting to unmark. Leaving used[i] = True after returning blocks the value from later positions.
  • Repeated values. With [1, 1, 2] this code returns duplicates. Sort first and skip a value equal to the previous one when the previous one is not currently used (LeetCode 47, Permutations II).
  • Using value in path instead of flags. It works for distinct values but costs O(n) per check and breaks when values repeat.
  • Growth. 10 values give 3,628,800 orderings. If you only need one ordering with a property, search with pruning instead of generating everything.

Where this shows up in data engineering

Rarely directly. The honest connection is in testing: you can check that a merge or aggregation is order-independent by running it on every permutation of a small input, and you can explain why join-order search in a query planner cannot try all n! orders for many tables and falls back on dynamic programming or heuristics.

By Data Career Hub Editorial · Last reviewed Oct 2026 · Python 3 solutions verified with assert-based tests

Progress is saved in this browser only. No account needed.

Search
Filter by type