🔁 Permutations #46
Given a vector nums of n distinct integers, generate all possible permutations (arrangements) of the elements of nums. Return a vector<vector<int>> containing all permutations in any order.
Constraints (typical):
1 <= n <= 8(factorial growth)- All elements are distinct (simplifies implementation)
There are two common, easy-to-understand approaches:
-
Backtracking with visited/frequency array Build a temporary array
ds(current permutation) by choosing one unused element at a time. Use a booleanfreq[]to mark which indices are already used. Recurse until permutation length equalsn, then record a copy. -
In-place swapping (generate permutations by swapping) Fix a position
indand try each candidate element to occupy that position by swapping it intoind. Recurse withind+1. After recursion, swap back (backtrack). This avoids extrafreq[]arrays and can be more memory-efficient because it builds permutations in-place.
Both produce the same set of permutations (order may differ) and are standard backtracking patterns.
A brute force idea would be: for every possible ordering of indices, create a permutation and check uniqueness. Practically this is the same as generating all n! permutations — there is no better lower bound than Ω(n!) because you must output n! permutations. So both backtracking methods are essentially optimal in asymptotic terms for this output size.
// Approach 1: Backtracking with visited/frequency array
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
vector<int> ds;
vector<bool> used(nums.size(), false);
backtrack(nums, used, ds, res);
return res;
}
private:
void backtrack(const vector<int> &nums, vector<bool> &used,
vector<int> &ds, vector<vector<int>> &res) {
if (ds.size() == nums.size()) {
res.push_back(ds); // record a copy
return;
}
for (size_t i = 0; i < nums.size(); ++i) {
if (used[i]) continue;
used[i] = true;
ds.push_back(nums[i]);
backtrack(nums, used, ds, res);
ds.pop_back();
used[i] = false;
}
}
};// Approach 2: In-place swapping
class Solution {
public:
vector<vector<int>> permute(vector<int>& nums) {
vector<vector<int>> res;
permuteInPlace(0, nums, res);
return res;
}
private:
void permuteInPlace(int ind, vector<int> &nums, vector<vector<int>> &res) {
if (ind == (int)nums.size()) {
res.push_back(nums); // push current ordering
return;
}
for (int i = ind; i < (int)nums.size(); ++i) {
swap(nums[i], nums[ind]);
permuteInPlace(ind + 1, nums, res);
swap(nums[i], nums[ind]); // backtrack
}
}
};- Backtracking with
used: At each recursion, you choose any unused element and append it. You explore alln!possible sequences because you try every candidate at every depth. - In-place swapping: Fixes one position at a time. For index
ind, swapping each candidate intoindensures all permutations with that prefix will be generated. After recursion you restore the array (swap back), so other orderings can be made.
They are standard textbook algorithms.
Let n = nums.size().
-
Number of permutations:
n! -
Time Complexity:
- Both algorithms must generate
n!permutations, each of lengthnto be copied to result →O(n * n!). - Overhead of recursion and swaps doesn't change the factorial factor. So time:
O(n * n!).
- Both algorithms must generate
-
Space Complexity:
-
Output storage:
O(n * n!)(to store all permutations). -
Additional recursion stack:
- Backtracking with
used:O(n)fords+O(n)forused+ recursion depthO(n)→O(n). - In-place swapping:
O(n)recursion depth andO(1)extra besides recursion →O(n).
- Backtracking with
-
So additional aux space (excluding output) is
O(n).
-
-
Small / distinct
- Input:
[1]→ Output:[[1]] - Input:
[1,2]→ Output:[[1,2],[2,1]]
- Input:
-
Three elements
- Input:
[1,2,3]→ 6 permutations (all unique)
- Input:
-
Negative / arbitrary
- Input:
[-1, 0, 1]→ works the same
- Input:
-
Larger n
- Input:
n = 8(max typical) → 40320 permutations — ensure you have memory/time to handle that.
- Input:
-
Edge: empty input (if allowed)
-
Input:
[]→ By convention, permutations of empty set is[[]](one permutation with zero elements). Current code: ifnums.size() == 0:- Backtracking version: will push an empty
dsonce →res = { { } }— correct. - In-place version:
ind == nums.size()is true at start, so it pushesnums(empty) once → correct.
- Backtracking version: will push an empty
-
-
Use
vector<int> ds; ds.reserve(nums.size());to avoid repeated reallocations in the backtracking approach. -
For lexicographic order of permutations, you can:
- Sort
numsfirst and for the in-place approach iterate withnext_permutationfrom STL as an alternative.
- Sort
-
If
numscan have duplicates, these algorithms produce duplicate permutations. To handle duplicates:- For backtracking with
used, sortnumsand skip duplicates with an extra check:if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue; - For in-place swapping, use a
setor only swap distinct values for a givenind(slightly more complex).
- For backtracking with
-
Use iterative STL approach: start with sorted
numsand callstd::next_permutationin a loop — easy and sometimes faster in practice.
- Permutations II (with duplicates) — produce unique permutations when input has repeated elements. Techniques: sort + skip duplicates.
- k-permutations — generate permutations of length
k(choosekelements and permute). Modify depth/stop condition. - Permutation ranking / unranking — convert between permutation and its lexicographic rank (combinatorial math).
- Generate permutations in lexicographic order — start sorted and use
next_permutation.
- Forgetting to backtrack (undo changes): e.g., not marking
used[i] = falseon returning or not swapping back. - Using
nums.size()in loop headers and usingint/size_tinconsistently — cast tointif indexes areint. - If elements are not distinct and you don't handle duplicates, you'll produce repeated permutations.
- Pushing references instead of copies (not an issue here —
vectorcopies contents intores).
Q: Which approach is faster?
A: They are similar asymptotically. The in-place swap approach often has slightly lower constant memory overhead because it avoids an extra
ds/usedarrays; it constructs permutations in the original array and copies on each leaf. For smallnboth are fine.
Q: Can we avoid copying the whole permutation when pushing to res?
A: Not if you must retain all permutations. You must store
n!vectors each of lengthn: copying is necessary to preserve each permutation's state. You can, however, move (std::move) if you can reuse buffers, but standard practice is to copy.
Q: Which is easier to modify for duplicates?
A: The backtracking-with-used approach is easier to extend to handle duplicates using the
if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;trick after sorting.
Q: What about iterative generation?
A: You can sort
numsand calldo { res.push_back(nums); } while (next_permutation(nums.begin(), nums.end()));— simple and robust (requires starting sorted to get all permutations).