Topics › Recursion and Backtracking › Trying out all Combos / Hard
Permutations of a String
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
You are given a string s. Return all of its permutations: every string that uses each character of s exactly once, in some order. In the classic version (the SDE sheet entry, which LeetCode 46 poses on an array of distinct integers) all characters are distinct, so there are exactly n! answers. If a character repeats, two different orderings of the positions can spell the same string; each distinct string must appear once (LeetCode 47, “Permutations II”). The answers may be returned in any order.
Constraints: 0 ≤ n ≤ 8. The output alone holds up to n! · n characters, so n is always small: at n = 8 there are already 40,320 permutations, and at n = 12 there would be 479 million.
Example 1
s = "abc"
answer = [abc, acb, bac, bca, cab, cba] // 3! = 6 arrangements of three distinct letters
Example 2
s = "aab"
answer = [aab, aba, baa] // 3! / 2! = 3; swapping the two a's creates nothing new
Approach 1: Brute force, pick any unused character
Intuition. Build the permutation one position at a time. The first position can hold any of the n characters, the second any of the n - 1 that remain, and so on. Recursion mirrors this exactly: keep a used flag for every index and a partial string cur. At each level, loop over all indices, take one that is not used yet, recurse, and then undo the choice so the next iteration starts from the same state. When cur reaches length n, every index has been used exactly once, so cur is a permutation.
Because the choice is made by index, not by value, a repeated letter produces the same string several times: in "aab", taking the first a then the second gives the same text as taking them the other way round. The brute force simply collects results in a set, which is correct but wasteful. For "aaaaaaa" it builds 5,040 strings to keep one.
Algorithm.
- Allocate
used[n], an emptyStringBuilder curand an empty setfound. build(): ifcur.length() == n, addcur.toString()tofoundand return.- For each index
iwithused[i] == false: mark it, appendchars[i], callbuild(), then delete the last character and clearused[i]. - Return the contents of
foundas a list.
static List<String> bruteForce(String s) {
Set<String> found = new LinkedHashSet<>(); // keeps generation order, drops duplicate arrangements
build(s.toCharArray(), new boolean[s.length()], new StringBuilder(), found);
return new ArrayList<>(found);
}
private static void build(char[] chars, boolean[] used, StringBuilder cur, Set<String> found) {
if (cur.length() == chars.length) { // every character placed exactly once
found.add(cur.toString());
return;
}
for (int i = 0; i < chars.length; i++) {
if (used[i]) continue;
used[i] = true;
cur.append(chars[i]);
build(chars, used, cur, found);
cur.deleteCharAt(cur.length() - 1); // undo: free the slot and the character
used[i] = false;
}
}
Complexity. Time O(n · n!). The recursion tree has n! / (n - k)! nodes at depth k; summed over all depths that is less than e · n! nodes, and each node runs a loop of n steps. Each of the n! leaves also spends O(n) to copy and hash its string. Space O(n) for used, cur and the recursion stack, on top of the O(n · n!) output (and the set that mirrors it).
Two details carry the whole method. cur.toString() makes a copy; adding the builder itself would store n! references to one object that ends up empty. And the undo lines must reverse the do lines exactly, in the opposite order, or the next loop iteration sees a corrupted state.
Approach 2: Better, swap characters into place
Intuition. The used array and the separate buffer can both disappear if the array keeps an invariant: positions 0 .. index-1 are already fixed, and a[index .. n-1] holds exactly the characters that are still free. To decide what goes at position index, swap each candidate a[i] with i ≥ index into that position, recurse on index + 1, then swap it back. Every call leaves the array exactly as it found it (each swap is undone before returning), so after the recursive call the suffix is restored and the next candidate is chosen from the same free set.
This structure also fixes duplicates at the source instead of after the fact. If two equal values are placed at the same position, the remaining free characters form the same multiset, so the two subtrees produce identical strings. A small set tried, local to each call, skips a value that has already been placed at index. Now any two leaves first differ at some position where they hold different values, so every output is distinct and no global set is needed. Nothing is lost either: every distinct free value is still tried at every position.
Algorithm.
permuteBySwapping(a, index): ifindex == n, addnew String(a)to the output and return.- Create an empty set
tried. - For
ifromindexton - 1: ifa[i]is already intried, skip it; otherwise add it, swapa[index]anda[i], recurse withindex + 1, and swap them back.
static List<String> better(String s) {
List<String> out = new ArrayList<>();
permuteBySwapping(s.toCharArray(), 0, out);
return out;
}
private static void permuteBySwapping(char[] a, int index, List<String> out) {
if (index == a.length) { // positions 0..n-1 are all fixed
out.add(new String(a));
return;
}
Set<Character> tried = new HashSet<>(); // values already placed at position `index`
for (int i = index; i < a.length; i++) {
if (!tried.add(a[i])) continue; // same value here would repeat a whole subtree
swap(a, index, i);
permuteBySwapping(a, index + 1, out);
swap(a, index, i); // restore so the next candidate sees the same suffix
}
}
private static void swap(char[] a, int i, int j) {
char t = a[i];
a[i] = a[j];
a[j] = t;
}
Complexity. Time O(n · n!) for distinct characters: it is the same tree shape as Approach 1, with the loop now running over the n - index free positions only. With repeats the tree contains only distinct prefixes. There are at most P of them at each depth, where P is the number of distinct permutations, so the work is O(n² · P) at worst, and for "aaaaaaa" the whole tree is a single path. Space O(n) recursion depth plus the tried sets along the current path (at most n - index entries at depth index), besides the output.
The output order is not lexicographic. For "abc" the swaps produce abc, acb, bac, bca, cba, cab: the last two appear in that order because the third branch starts from cba after swapping a and c. If sorted output is required, Approach 3 gives it for free.
Approach 3: Optimal, iterate with next permutation
Intuition. Order all distinct arrangements lexicographically. Every arrangement except the largest has a unique successor, and it can be computed in place in O(n) without recursion. So start from the sorted string, which is the smallest arrangement, emit it, step to its successor, and repeat until the current string is the largest one (characters in non-increasing order). No recursion, no hash set, and the output comes out sorted.
Computing the successor:
- Find the rightmost
iwitha[i] < a[i + 1]. Everything to the right ofiis non-increasing, which means it is already the largest possible arrangement of those characters. Nothing confined to that suffix can make the string bigger, so positionihas to change. If no suchiexists, the whole string is non-increasing and this was the last permutation. - Find the rightmost
j > iwitha[j] > a[i]. Because the suffix is non-increasing, this is the smallest character in the suffix that is still bigger thana[i]. Swapa[i]anda[j]: the string grows by the smallest possible amount at positioni. - The suffix is still non-increasing after the swap. Reverse it so it becomes ascending, the smallest arrangement of those characters.
Duplicates need no special care. The strict comparisons (>= while scanning for i, <= while scanning for j) guarantee a strictly larger character moves into position i, so each step yields a strictly larger string. Because it is the immediate successor, no distinct string is skipped, and none repeats.
static List<String> optimal(String s) {
char[] a = s.toCharArray();
Arrays.sort(a); // the smallest arrangement comes first
List<String> out = new ArrayList<>();
do {
out.add(new String(a));
} while (nextPermutation(a));
return out;
}
private static boolean nextPermutation(char[] a) {
int i = a.length - 2;
while (i >= 0 && a[i] >= a[i + 1]) i--; // a[i+1..] is the longest non-increasing suffix
if (i < 0) return false; // whole array non-increasing: this was the last one
int j = a.length - 1;
while (a[j] <= a[i]) j--; // rightmost character strictly bigger than a[i]
swap(a, i, j);
for (int lo = i + 1, hi = a.length - 1; lo < hi; lo++, hi--) swap(a, lo, hi); // make the suffix ascending
return true;
}
The swap helper is the one from Approach 2.
Dry run on Example 2, s = "aab" (already sorted):
| current | emitted | i (rightmost a[i] < a[i+1]) |
j (rightmost a[j] > a[i]) |
after swap | after reversing a[i+1..] |
|---|---|---|---|---|---|
aab |
aab |
1 (a < b) |
2 (b) |
aba |
aba |
aba |
aba |
0 (a < b; b ≥ a at index 1) |
1 (b; index 2 holds a, not bigger) |
baa |
baa |
baa |
baa |
none | stop |
Three strings for three distinct arrangements; the duplicate a never caused a repeat. On "abc" the same loop emits abc, acb, bac, bca, cab, cba, sorted.
Complexity. Time O(n · P + n log n), where P ≤ n! is the number of distinct permutations: each step scans and reverses at most n characters and copies n characters into a new string, and the initial sort is O(n log n). Space O(1) beyond the character array and the output: no recursion stack, no sets.
All three methods are O(n · n!) on distinct input, which is the size of the output and therefore unavoidable. The differences show up on repeated characters (Approach 1 still does n! work), in memory (sets and recursion versus none), and in the order of the answer.
Edge cases and pitfalls
- Empty string. There is exactly one arrangement of nothing (
0! = 1), the empty string, and all three methods return[""]. Some judges expect an empty list instead; change the base case if so. - One character. Returns the string itself; the swap loop runs once and
nextPermutationfinds noi. - Repeated characters. Approach 1 removes repeats after generating them, Approach 2 prevents them per position, Approach 3 never creates them. A common alternative for Approach 1 is to sort the characters and skip index
iwhenchars[i] == chars[i - 1]andused[i - 1]is false. That rule relies on equal values being adjacent, so it does not carry over to the swap method, where swapping breaks the sorted order; use the per-position set there. - Forgetting to undo. Skipping the
deleteCharAt, theused[i] = false, or the second swap leaves the shared state wrong for every later branch, and the output silently becomes garbage. - Storing a live buffer. Always store a copy (
toString(),new String(a),new ArrayList<>(cur)). - Character order.
Arrays.sortorders by UTF-16 code, so uppercase letters sort before lowercase: the first permutation of"aBa"is"Baa". - Growth.
n!explodes. Ten characters already give 3.6 million strings; generating permutations is only reasonable for very short inputs.
Related problems
- Next Permutation is exactly the successor step used in Approach 3.
- Permutation Sequence jumps straight to the
k-th permutation without generating the ones before it. - Subsets II uses the same “skip an equal value at the same level” idea to avoid duplicate outputs.
- Combination Sum II applies that duplicate-skip rule to combinations.
- N Queen is a permutation search in disguise: one queen per row, each column used once, with pruning.
- Word Break (print all ways) is another backtracking problem that lists every valid answer.