String Permutations
Build all character arrangements using recursive swapping and backtracking
Understanding String Permutations: Foundations and Intuition
Have you ever stood in front of a combination lock, frantically trying different arrangements of the same digits? Or wondered how many ways you could rearrange the letters of your name? This intuitive puzzle—exploring all possible arrangements of a set of items—is at the heart of one of the most fundamental algorithmic patterns you'll encounter in coding interviews. String permutations appear in countless LeetCode problems, and mastering them unlocks a powerful problem-solving toolkit. In this lesson, we'll build a rock-solid foundation for understanding permutations, explore the elegant backtracking pattern that generates them, and provide free flashcards to cement these concepts in your memory.
But why should you care about permutations beyond passing interviews? Understanding this pattern trains your brain to think recursively, to visualize decision trees, and to recognize when a brute-force exploration of possibilities is actually the optimal solution. Let's dive deep into the foundations.
What Are String Permutations?
Permutations are all possible arrangements of a set of elements where order matters. When we talk about string permutations, we mean every unique way to rearrange the characters in that string. For example, the string "ABC" has exactly 6 permutations:
ABC, ACB, BAC, BCA, CAB, CBA
Notice that each permutation uses every character exactly once, and changing the order creates a different permutation—"ABC" is distinct from "CBA" even though they contain the same letters.
🎯 Key Principle: A permutation is an ordered arrangement. The permutation problem asks: "In how many different orders can I arrange these elements?"
The mathematical formula for calculating the number of permutations is beautifully simple: for a string with n unique characters, there are n! (n factorial) possible permutations. Let's understand why:
- For the first position, you have n choices
- For the second position, you have n-1 remaining choices
- For the third position, you have n-2 remaining choices
- And so on...
Multiplying these together gives us: n × (n-1) × (n-2) × ... × 2 × 1 = n!
💡 Real-World Example: For "ABC" with 3 characters: 3! = 3 × 2 × 1 = 6 permutations. For "ABCD" with 4 characters: 4! = 4 × 3 × 2 × 1 = 24 permutations. Notice how quickly this grows—a string with just 10 characters has 3,628,800 permutations!
🤔 Did you know? The factorial function grows faster than exponential functions. This explosive growth is why permutation problems are computationally expensive and why understanding their complexity is crucial for interview discussions.
Visualizing the Decision Tree Model
The most powerful mental model for understanding permutations is the decision tree. This tree visualizes the backtracking process as a series of choices, where each level of the tree represents a position in the string we're building.
Let's visualize generating permutations of "ABC":
[Start]
/ | \
A/ B| \C
/ | \
[A] [B] [C]
/ \ / \ / \
B/ \C B/ \C A/ \B
/ \ / \ / \
[AB] [AC][BA] [BC][CA] [CB]
| | | | | |
C B C A B A
| | | | | |
[ABC] [ACB][BAC][BCA][CAB][CBA]
Let's break down what's happening at each level:
Level 1 (Root): We start with an empty string and must choose which character to place first. We have 3 choices: A, B, or C.
Level 2: After choosing the first character, we have 2 remaining characters to choose from for the second position.
Level 3: After choosing two characters, only 1 remains for the final position.
Leaf Nodes: The bottom level contains our complete permutations—each path from root to leaf represents one valid arrangement.
💡 Mental Model: Think of backtracking as a depth-first exploration of this tree. You go down one path until you reach a complete permutation (a leaf), record it, then backtrack up to try the next branch. The term "backtracking" literally means backing up in the tree to explore alternative choices.
🧠 Mnemonic: "Backtracking = Building paths, Backing up, Branching out."
Here's a simple visualization showing how we track our state:
Building permutations of "ABC":
Step 1: Choose 'A' current = "A" remaining = "BC"
Step 2: Choose 'B' current = "AB" remaining = "C"
Step 3: Choose 'C' current = "ABC" remaining = ""
→ Found permutation: "ABC"
Step 4: Backtrack to "AB", no more choices
Step 5: Backtrack to "A", try next choice
Step 6: Choose 'C' current = "AC" remaining = "B"
...
Unique vs. Duplicate Characters: A Critical Distinction
So far, we've assumed all characters in our string are unique. But what happens when we have duplicate characters? This is where things get interesting—and where many candidates stumble in interviews.
Consider the string "AAB". If we blindly apply our formula, we'd calculate 3! = 6 permutations. But let's list them:
Using naive approach:
AAB, AAB, ABA, ABA, BAA, BAA
We get duplicates! The two A's are indistinguishable, so swapping them doesn't create a new permutation. The actual unique permutations are:
AAB, ABA, BAA
Only 3 permutations, not 6.
🎯 Key Principle: When duplicate characters exist, the formula becomes: n! / (c₁! × c₂! × ... × cₖ!) where c₁, c₂, ..., cₖ are the counts of each duplicate character.
For "AAB": 3! / (2! × 1!) = 6 / 2 = 3 permutations ✓
For "AABB": 4! / (2! × 2!) = 24 / 4 = 6 permutations
💡 Pro Tip: In coding interviews, always clarify whether the input string contains duplicates. This determines whether you need the simpler "unique characters" algorithm or the more complex "duplicate handling" version.
Here's how the decision tree changes with duplicates in "AAB":
[Start]
/ | \
A/ A| \B
/ | \
[A] [A] [B]
/ \ / \ / \
A/ \B A/ \B A/ \A
/ \/ \/ \
[AA] [AB][AA][AB][BA][BA]
| | | | | |
B A B A A A
| | | | | |
[AAB] [ABA][AAB][ABA][BAA][BAA]
↑ Duplicates!
The solution? We need to skip branches that would lead to duplicate permutations. We do this by:
- Sorting the string first
- Skipping a character if it's the same as the previous character AND the previous character hasn't been used yet
This pruning strategy transforms our decision tree, cutting off duplicate branches early.
⚠️ Common Mistake 1: Forgetting to sort the input when handling duplicates. The duplicate-detection logic relies on identical characters being adjacent. ⚠️
⚠️ Common Mistake 2: Using a Set to remove duplicates after generating all permutations. While this works, it's inefficient—you're generating duplicates only to discard them. Better to avoid generating them in the first place. ⚠️
Time and Space Complexity: Understanding the Cost
Let's analyze the computational complexity of generating string permutations. This is crucial for interview discussions where you'll need to justify your approach.
Time Complexity: O(n! × n)
This might look intimidating, so let's break it down:
- n!: We generate n! permutations (for unique characters)
- × n: For each permutation, we need O(n) time to build/copy the string
The n! dominates the complexity, making this an extremely expensive operation for large inputs.
## Illustrating why it's O(n! × n)
def generate_permutations(s):
result = []
def backtrack(current, remaining):
if not remaining:
result.append(current) # O(n) to copy the string
return
for i in range(len(remaining)): # This loop executes n! times total
backtrack(
current + remaining[i],
remaining[:i] + remaining[i+1:]
)
backtrack("", s)
return result # Returns n! permutations
❌ Wrong thinking: "The time complexity is O(n) because we just loop through the characters."
✅ Correct thinking: "Each recursive call branches into multiple new calls. The total number of complete permutations we generate is n!, and each one takes O(n) time to construct."
Space Complexity: O(n)
The space complexity depends on what we're measuring:
- Recursion stack depth: O(n) — we go n levels deep in our decision tree
- Storing the result: O(n! × n) — we store n! strings, each of length n
In interviews, when discussing space complexity for backtracking, we typically focus on the recursion stack and auxiliary space used by the algorithm itself, not the output. So we say O(n).
🧠 Mnemonic: "Stack height = String length" for recursion depth.
Real-World Applications and Interview Scenarios
Understanding string permutations isn't just academic—it appears in numerous practical LeetCode problems and real-world scenarios:
Common LeetCode Problems:
🔧 LeetCode 46 - Permutations: Generate all permutations of an array of unique integers
🔧 LeetCode 47 - Permutations II: Generate all unique permutations with duplicates
🔧 LeetCode 60 - Permutation Sequence: Find the kth permutation without generating all of them
🔧 LeetCode 567 - Permutation in String: Determine if one string is a permutation of another's substring
🔧 LeetCode 31 - Next Permutation: Find the next lexicographically greater permutation
💡 Real-World Example: Permutation concepts apply to:
- Password cracking: Trying all possible arrangements of known characters
- Scheduling problems: Arranging tasks or appointments in different orders
- Game AI: Exploring all possible move sequences
- Cryptography: Substitution ciphers rely on character permutations
- DNA sequencing: Analyzing possible arrangements of genetic markers
Here's a practical example showing how permutation thinking solves a common interview problem:
def checkInclusion(s1, s2):
"""
LeetCode 567: Check if s2 contains a permutation of s1.
Key insight: Instead of generating all permutations of s1 (expensive!),
we check if any substring of s2 has the same character frequency.
This demonstrates how understanding permutations helps you recognize
when NOT to generate them explicitly!
"""
from collections import Counter
if len(s1) > len(s2):
return False
s1_count = Counter(s1)
window_count = Counter(s2[:len(s1)])
if s1_count == window_count:
return True
# Sliding window
for i in range(len(s1), len(s2)):
window_count[s2[i]] += 1
window_count[s2[i - len(s1)]] -= 1
if window_count[s2[i - len(s1)]] == 0:
del window_count[s2[i - len(s1)]]
if window_count == s1_count:
return True
return False
💡 Pro Tip: In interviews, recognizing that a problem involves permutations but doesn't require generating them all is a sign of algorithmic maturity. Always ask: "Do I need all permutations, or just need to check a property?"
Building Your Mental Framework
Before moving to implementation, let's consolidate our understanding with a comprehensive reference:
📋 Quick Reference Card: Permutation Fundamentals
| 🎯 Concept | 📝 Description | 🔢 Formula/Complexity |
|---|---|---|
| 🔑 Definition | All ordered arrangements of elements | Order matters |
| 📊 Count (unique) | Number of arrangements with distinct items | n! |
| 📊 Count (duplicates) | Number of unique arrangements with repeats | n! / (c₁! × c₂! × ... × cₖ!) |
| ⏱️ Time Complexity | Cost to generate all permutations | O(n! × n) |
| 💾 Space (stack) | Recursion depth | O(n) |
| 🌳 Mental Model | Decision tree with backtracking | Depth-first exploration |
| 🎨 Key Pattern | Choose → Explore → Unchoose | Backtracking template |
✅ Core Understanding Checklist:
Before moving to implementation, ensure you can:
🧠 Explain why the formula is n! in your own words
🧠 Draw a decision tree for a 3-character string
🧠 Identify when duplicates require special handling
🧠 Calculate time complexity with justification
🧠 Recognize permutation problems that don't need generation
Connecting to the Backtracking Pattern
String permutations are the perfect introduction to backtracking because they exemplify the pattern's core structure:
1. CHOOSE: Select an element to add to current permutation
2. EXPLORE: Recursively build the rest of the permutation
3. UNCHOOSE: Remove the element to try other options (backtrack)
This choose-explore-unchoose pattern appears in countless algorithmic problems:
- Generating combinations
- Solving Sudoku
- N-Queens problem
- Subset generation
- Path finding with constraints
🎯 Key Principle: Master permutations, and you've mastered the backtracking template that unlocks dozens of other problems.
In the next section, we'll translate this conceptual understanding into working code, implementing both the swap-based and used-array approaches step by step. You'll see exactly how the decision tree translates into recursive function calls, and how to handle both unique and duplicate characters efficiently.
With this foundation in place, you're ready to see how elegant code can systematically explore the entire decision space. The mathematical understanding you've built here will make the implementation intuitive rather than mysterious.
Implementing the Backtracking Algorithm
Now that we understand the conceptual foundation of string permutations, let's translate that understanding into working code. The backtracking approach gives us an elegant way to explore all possible permutations by making choices, exploring their consequences, and then undoing those choices to try alternatives. In this section, we'll explore two fundamental implementations that represent different ways to track our state during the recursive exploration.
The Swap-Based Approach: In-Place Permutation Generation
The swap-based approach is one of the most elegant implementations of permutation generation. The core insight is that we can generate all permutations by systematically swapping characters into each position. Think of it as building a permutation from left to right: for each position, we try every remaining character by swapping it into that position.
🎯 Key Principle: In the swap-based approach, we use the position in the string itself to track our progress. Characters before our current index represent choices we've already made, while characters from our current index onward represent available choices.
Here's how the algorithm flows:
Original: [A, B, C] (index 0)
^
We try each character at position 0
Choice 1: [A, B, C] (keep A, recurse with index 1)
|
+---> [A, B, C] (keep B at index 1, recurse with index 2)
| | [A, B, C] ✓ Base case reached!
|
+---> [A, C, B] (swap C to index 1, recurse with index 2)
[A, C, B] ✓ Base case reached!
Let's implement this approach:
def permute_swap(s):
"""
Generate all permutations using the swap-based backtracking approach.
Time: O(n! * n), Space: O(n) for recursion stack
"""
result = []
chars = list(s) # Convert to list for in-place swapping
def backtrack(start):
# Base case: when start reaches the end, we have a complete permutation
if start == len(chars):
result.append(''.join(chars))
return
# Try each character from start to end at position 'start'
for i in range(start, len(chars)):
# Make choice: swap character at i to position start
chars[start], chars[i] = chars[i], chars[start]
# Explore: recursively build the rest of the permutation
backtrack(start + 1)
# Undo choice: swap back to restore original state
chars[start], chars[i] = chars[i], chars[start]
backtrack(0)
return result
## Example usage
print(permute_swap("ABC"))
## Output: ['ABC', 'ACB', 'BAC', 'BCA', 'CAB', 'CBA']
Let's trace through a critical moment in the execution. When we call permute_swap("AB"), here's what happens:
Call Stack chars state Action
-----------------------------------------------------------------
backtrack(0) [A, B] Try i=0: swap(0,0)
backtrack(1) [A, B] Try i=1: swap(1,1)
backtrack(2) [A, B] Base case! Add "AB"
<return> [A, B] Undo swap(1,1)
<done with loop> [A, B] Try i=1: swap(0,1)
backtrack(1) [B, A] Try i=1: swap(1,1)
backtrack(2) [B, A] Base case! Add "BA"
<return> [B, A] Undo swap(1,1)
<done with loop> [B, A] Undo swap(0,1)
<return> [A, B] Back to original
💡 Pro Tip: The swap-before-recurse and swap-after-return pattern is the signature of backtracking. The second swap isn't just cleanup—it's essential for correctness because we're reusing the same array throughout our exploration.
⚠️ Common Mistake: Forgetting the second swap (the "undo" step) leads to corrupted state. Remember: each recursive call should see the array in the same state it would have seen if we'd made fresh copies at each level. Mistake 1: Writing backtrack(start + 1) without the corresponding undo swap afterward. This breaks the exploration of alternative choices. ⚠️
The Boolean Visited Array Approach: Clearer State Management
While the swap-based approach is elegant, the visited array approach offers clearer state management and is often easier to extend for more complex problems. Instead of modifying the input in-place, we build permutations character by character while tracking which characters we've already used.
🎯 Key Principle: The visited array approach separates the "available choices" tracking from the "current permutation being built" tracking. This separation makes the algorithm easier to understand and modify.
The mental model here is different: imagine you have a bucket of characters and a blank form to fill out. At each position in the form, you look at all characters in the bucket, pick one that hasn't been used (checked via the visited array), write it down, mark it as used, and move to the next position. When you're done filling out the form, you have one complete permutation.
def permute_visited(s):
"""
Generate all permutations using a boolean visited array to track used characters.
Time: O(n! * n), Space: O(n) for visited array + current path
"""
result = []
visited = [False] * len(s)
current = []
def backtrack():
# Base case: current permutation is complete
if len(current) == len(s):
result.append(''.join(current))
return
# Try each character that hasn't been used yet
for i in range(len(s)):
if visited[i]:
continue # Skip already-used characters
# Make choice: add character and mark as used
current.append(s[i])
visited[i] = True
# Explore: build rest of permutation
backtrack()
# Undo choice: remove character and mark as unused
current.pop()
visited[i] = False
backtrack()
return result
## Example usage
print(permute_visited("ABC"))
## Output: ['ABC', 'ACB', 'BAC', 'BCA', 'CAB', 'CBA']
Here's a detailed trace showing how the state evolves:
Call current visited Action
---- ------- ------------- ---------------------------
1 [] [F, F, F] Try index 0 (A)
2 [A] [T, F, F] Try index 1 (B)
3 [A,B] [T, T, F] Try index 2 (C)
4 [A,B,C] [T, T, T] Base case! Save "ABC"
<-3 [A,B] [T, T, F] Backtrack, undo C
<-2 [A] [T, F, F] Backtrack, undo B
2b [A] [T, F, F] Try index 2 (C)
3b [A,C] [T, F, T] Try index 1 (B)
4b [A,C,B] [T, T, T] Base case! Save "ACB"
...
💡 Mental Model: Think of current as your "shopping cart" where you're collecting characters, and visited as tags on each character saying "already in cart" or "available to add."
Handling Duplicate Characters: The Sorting and Skip Pattern
When the input string contains duplicate characters, both approaches above will generate duplicate permutations. For example, permuting "AAB" would produce "AAB" twice through different paths. We need to modify our algorithm to skip redundant branches.
🎯 Key Principle: To avoid duplicate permutations, we must avoid making the same choice at the same decision point. If two characters are the same, picking either one leads to identical subtrees.
The solution involves two steps:
- Sort the input string so identical characters are adjacent
- Skip a character if it's the same as the previous character and the previous character hasn't been used yet
⚠️ Common Mistake: The condition visited[i-1] == False is crucial and counter-intuitive. You might think "skip if the previous identical character WAS used," but it's the opposite! Mistake 2: Using visited[i-1] == True in the skip condition generates duplicates because it doesn't enforce a consistent ordering among identical elements. ⚠️
Here's why the condition works: By requiring that identical characters are used in order (left to right), we ensure each unique permutation is generated exactly once. If we haven't used the first 'A' yet (visited[i-1] == False), we shouldn't use the second 'A'.
def permute_unique(s):
"""
Generate unique permutations from a string with duplicate characters.
Time: O(n! * n) worst case, but prunes many branches
Space: O(n) for visited array and current path
"""
result = []
chars = sorted(s) # Sort to group identical characters together
visited = [False] * len(chars)
current = []
def backtrack():
# Base case: complete permutation found
if len(current) == len(chars):
result.append(''.join(current))
return
for i in range(len(chars)):
# Skip if already used
if visited[i]:
continue
# Skip duplicates: if current char equals previous char
# and previous char hasn't been used, skip this char
if i > 0 and chars[i] == chars[i-1] and not visited[i-1]:
continue
# Make choice
current.append(chars[i])
visited[i] = True
# Explore
backtrack()
# Undo choice
current.pop()
visited[i] = False
backtrack()
return result
## Example with duplicates
print(permute_unique("AAB"))
## Output: ['AAB', 'ABA', 'BAA'] - only 3 unique permutations, not 6
Let's visualize why this works with "AAB":
Without duplicate handling:
root
/ | \
A₁ A₂ B
/ \ / \ / \
A₂ B A₁ B A₁ A₂ <- Notice A₁BA₂ and A₂BA₁ are identical "ABA"
With duplicate handling:
root
/ | \
A₁ X B <- A₂ skipped because A₁ not used yet
/ \ / \
A₂ B A₁ X <- A₂ skipped for same reason
| | |
B A₂ A₂ <- Now each unique permutation appears once
💡 Pro Tip: The sorting step is essential! Without it, identical characters might not be adjacent, and our skip condition wouldn't catch all duplicates. Always sort first when dealing with duplicates.
📋 Quick Reference Card: Approach Comparison
| Aspect | 🔄 Swap-Based | ✓ Visited Array |
|---|---|---|
| 🧠 State Management | In-place modification | Explicit tracking |
| 💾 Space Complexity | O(n) recursion only | O(n) visited + current |
| 🎯 Base Case | start == length | len(current) == length |
| 🔧 Extensibility | Harder to modify | Easier to extend |
| 📚 Duplicate Handling | Complex | Natural with sort+skip |
| ⚡ Performance | Slightly faster | More readable |
Understanding the Base Case and Result Building
The base case is where our recursion bottoms out and we actually capture a complete permutation. Identifying the correct base case is crucial—it's the moment when we transition from "building" to "collecting."
In the swap-based approach, the base case is start == len(chars) because we've made decisions for every position. In the visited array approach, it's len(current) == len(s) because we've collected all characters.
❌ Wrong thinking: "The base case is when we run out of choices." ✅ Correct thinking: "The base case is when we've built a complete solution of the required length."
When we reach the base case, we must build the result by adding our current permutation to the result list. A subtle but critical point: we must create a copy of the current state, not store a reference to it.
⚠️ Common Mistake: In Python, writing result.append(current) instead of result.append(''.join(current)) or result.append(current[:]) will store a reference to the mutable list, which gets modified by backtracking. You'll end up with a result list full of empty arrays! Mistake 3: Forgetting that lists are mutable and passed by reference in Python. ⚠️
🧠 Mnemonic: BASE = Build, Add, Snapshot, Exit
- Build: Verify the solution is complete
- Add: Append to result list
- Snapshot: Make a copy, not a reference
- Exit: Return to explore other branches
💡 Remember: The base case doesn't just stop recursion—it's where the actual work of collecting results happens. Every path through the recursion tree that reaches a base case contributes one permutation to your final answer.
Both approaches we've covered represent fundamental patterns you'll see throughout backtracking problems. The swap-based approach emphasizes efficiency and in-place manipulation, while the visited array approach emphasizes clarity and extensibility. Understanding both gives you flexibility to choose the right tool for each problem you encounter.
🤔 Did you know? The number of permutations of n items grows factorially (n!). For a string of length 10, that's 3,628,800 permutations. This explosive growth is why understanding the pruning techniques for duplicates becomes so important—eliminating even a small branch early can save enormous amounts of work.
With these two fundamental implementations in your toolkit, you're ready to tackle a wide variety of permutation problems. The patterns you've learned here—the make-explore-undo structure, the importance of state management, and the techniques for handling duplicates—will serve as building blocks for more complex backtracking challenges.
Optimization Patterns and Variations
Now that you understand the fundamentals of string permutations and can implement basic backtracking solutions, it's time to elevate your skills with optimization techniques that will make your code interview-ready. In this section, we'll explore how to transform a correct-but-slow solution into an efficient, production-quality implementation.
Pruning Techniques: Cutting the Decision Tree
Pruning is the art of recognizing when a branch of your decision tree cannot possibly lead to a valid solution, allowing you to skip it entirely. This can dramatically reduce the number of recursive calls from factorial complexity to something more manageable.
Consider the classic duplicate elimination problem. If your input string is "AAB", a naive backtracking approach generates duplicate permutations. The key insight is this: if we've already placed a character at a position, we should never place an identical character at that same position in the same recursive frame.
def permuteUnique(s):
"""
Generate unique permutations by pruning duplicates at each level.
Time: O(n! / (n1! * n2! * ... * nk!)) where ni is count of each unique char
"""
result = []
chars = sorted(list(s)) # Sorting brings duplicates together
used = [False] * len(chars)
def backtrack(current):
if len(current) == len(chars):
result.append(''.join(current))
return
for i in range(len(chars)):
# Skip if already used
if used[i]:
continue
# 🎯 Key Principle: Pruning duplicates
# If current char equals previous char AND previous char wasn't used,
# skip to avoid duplicate permutations
if i > 0 and chars[i] == chars[i-1] and not used[i-1]:
continue
used[i] = True
current.append(chars[i])
backtrack(current)
current.pop()
used[i] = False
backtrack([])
return result
The magic happens in the pruning condition. By sorting first, identical characters become adjacent. When we encounter a duplicate character, we only use it if its predecessor has already been used in the current path. This ensures we generate each unique permutation exactly once.
Visualization for "AAB":
Without pruning: With pruning:
root root
/ | \ / | \
A A B A _ B
/| |\ |\ /| |\
A B B A B A A B A A
| | | | | | | | | |
B A A B A B B A A B
6 permutations 3 unique permutations
(AAB, ABA, AAB, (AAB, ABA, BAA)
ABA, BAA, BAA)
💡 Pro Tip: Always sort your input when dealing with duplicate characters. This single step enables efficient pruning and often simplifies your logic.
The Next Permutation Algorithm: An Iterative Alternative
While recursion is elegant, the next permutation algorithm offers an iterative approach that's more space-efficient and often faster for generating all permutations in lexicographic order. This algorithm is based on a brilliant observation: given a permutation, we can find the next lexicographically larger permutation in O(n) time.
🎯 Key Principle: The next permutation algorithm finds the rightmost position where we can make a "small increase" to get the next larger permutation.
Here's the algorithm's logic:
Step-by-step for "1234" → "1243":
1. Find rightmost ascending pair: 1 2 3 4
↑ ↑
(3 < 4)
2. Find smallest element > 3 1 2 3 4
to the right of position 2: ↑
(4 is it)
3. Swap them: 1 2 4 3
4. Reverse everything after 1 2 4 3
position 2: ↑───↑
(already sorted)
Here's the implementation:
public class PermutationIterative {
/**
* Generate all permutations using next permutation algorithm.
* Space: O(n) - only stores current permutation
* Time: O(n! * n) - n! permutations, each requiring O(n) operations
*/
public List<String> permute(String s) {
List<String> result = new ArrayList<>();
char[] chars = s.toCharArray();
Arrays.sort(chars); // Start with smallest permutation
result.add(new String(chars));
while (nextPermutation(chars)) {
result.add(new String(chars));
}
return result;
}
private boolean nextPermutation(char[] arr) {
// Step 1: Find rightmost pair where arr[i] < arr[i+1]
int i = arr.length - 2;
while (i >= 0 && arr[i] >= arr[i + 1]) {
i--;
}
// If no such pair exists, we're at the last permutation
if (i < 0) {
return false;
}
// Step 2: Find smallest element > arr[i] to the right
int j = arr.length - 1;
while (arr[j] <= arr[i]) {
j--;
}
// Step 3: Swap
swap(arr, i, j);
// Step 4: Reverse everything after position i
reverse(arr, i + 1, arr.length - 1);
return true;
}
private void swap(char[] arr, int i, int j) {
char temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
private void reverse(char[] arr, int start, int end) {
while (start < end) {
swap(arr, start++, end--);
}
}
}
⚠️ Common Mistake: Forgetting to sort the input array before starting the next permutation algorithm. Without sorting, you won't generate all permutations—only those lexicographically larger than your starting configuration. ⚠️
💡 Real-World Example: The C++ STL function std::next_permutation uses exactly this algorithm. It's used in competitive programming and optimization problems where you need to iterate through all possible arrangements.
Common Variations: Expanding Your Toolkit
Let's explore variations you'll encounter in interviews, each requiring a slight modification to the basic approach.
K-Length Permutations
Instead of generating all permutations, you might need permutations of length k. This is common in combination lock problems or password generation scenarios.
def kPermutations(s, k):
"""
Generate all k-length permutations of string s.
For s="ABC", k=2: returns ["AB", "AC", "BA", "BC", "CA", "CB"]
"""
result = []
used = [False] * len(s)
def backtrack(current):
# Base case: we've reached desired length
if len(current) == k:
result.append(''.join(current))
return
for i in range(len(s)):
if used[i]:
continue
used[i] = True
current.append(s[i])
backtrack(current)
current.pop()
used[i] = False
backtrack([])
return result
The only change is the base case condition. Instead of len(current) == len(s), we check len(current) == k. This simple modification changes the complexity from O(n!) to O(n!/(n-k)!), which is substantially faster when k is small.
Permutations with Constraints
Sometimes you need permutations that satisfy specific rules. For example: "Generate permutations where no two adjacent characters are the same" or "Generate permutations where vowels must be in odd positions."
The pattern is to add a validation check before making each recursive call:
def constrainedPermutations(s):
"""
Generate permutations where no adjacent characters are identical.
Example: "AAB" → ["ABA"] only (AAB and BAA violate constraint)
"""
result = []
used = [False] * len(s)
def isValid(current, nextChar):
"""Check if adding nextChar maintains constraint"""
if not current:
return True
return current[-1] != nextChar
def backtrack(current):
if len(current) == len(s):
result.append(''.join(current))
return
for i in range(len(s)):
if used[i]:
continue
# 🎯 Pruning based on constraint
if not isValid(current, s[i]):
continue
used[i] = True
current.append(s[i])
backtrack(current)
current.pop()
used[i] = False
backtrack([])
return result
🧠 Mental Model: Think of constraints as additional pruning opportunities. Each constraint eliminates branches from your decision tree, making your solution faster.
Performance Optimization: StringBuilder vs String Concatenation
In Java, choosing the right string building approach can dramatically affect performance. Let's compare:
// ❌ Wrong thinking: String concatenation in loops
public void backtrackSlow(String current, Set<Character> used, String s) {
if (current.length() == s.length()) {
result.add(current);
return;
}
for (int i = 0; i < s.length(); i++) {
if (used.contains(s.charAt(i))) continue;
// String concatenation creates new String object each time!
used.add(s.charAt(i));
backtrackSlow(current + s.charAt(i), used, s); // O(n) operation
used.remove(s.charAt(i));
}
}
// ✅ Correct thinking: StringBuilder for efficient building
public void backtrackFast(StringBuilder current, boolean[] used, char[] s) {
if (current.length() == s.length) {
result.add(current.toString());
return;
}
for (int i = 0; i < s.length; i++) {
if (used[i]) continue;
used[i] = true;
current.append(s[i]); // O(1) amortized operation
backtrackFast(current, used, s);
current.deleteCharAt(current.length() - 1);
used[i] = false;
}
}
Why does this matter? String concatenation in Java creates a new String object each time because strings are immutable. For a permutation of length n, this means creating O(n) new objects per path, resulting in O(n! * n²) total operations. StringBuilder modifies in-place, reducing this to O(n! * n).
📋 Quick Reference Card: String Building Approaches
| Approach | 🚀 Time per append | 💾 Space overhead | 🎯 Best for |
|---|---|---|---|
| String concatenation | O(n) | O(n! * n²) | Never in backtracking |
| StringBuilder | O(1) amortized | O(n) | Java permutations |
| char[] array | O(1) | O(n) | When converting to array anyway |
| List |
O(1) amortized | O(n) + pointer overhead | Python/flexible sizing |
Converting Between Input/Output Formats
Interviews often require converting between different data structures. Here are the essential patterns:
// String → char array (fastest for iteration)
char[] chars = s.toCharArray();
// char array → String (for output)
String result = new String(chars);
// OR
String result = String.valueOf(chars);
// String → List<Character> (when you need List operations)
List<Character> list = new ArrayList<>();
for (char c : s.toCharArray()) {
list.add(c);
}
// List<Character> → String
String result = list.stream()
.map(String::valueOf)
.collect(Collectors.joining());
// OR more efficiently:
StringBuilder sb = new StringBuilder();
for (char c : list) {
sb.append(c);
}
String result = sb.toString();
// String[] → List<String>
List<String> list = Arrays.asList(array); // Fixed-size
// OR
List<String> list = new ArrayList<>(Arrays.asList(array)); // Mutable
💡 Pro Tip: When your solution works with char arrays internally but needs to return List
Lexicographic Ordering: Controlling Output Sequence
Lexicographic ordering (dictionary order) is crucial when problems specify "return permutations in sorted order" or "find the kth permutation."
The key insight: if you iterate through choices in sorted order and use the used-array approach (not swap-based), you automatically generate permutations lexicographically.
For "ABC" with sorted iteration:
Level 0: root
/ | \
A B C (iterate A→B→C)
/|\ |\ |\
Level 1: B C A C A B (at each level, skip used chars)
| | | | | |
Level 2: C B C A B A (complete permutations)
Output: [ABC, ACB, BAC, BCA, CAB, CBA] ← Lexicographic!
For the "kth permutation" problem, you can optimize further using the factorial number system:
def getKthPermutation(n, k):
"""
Find kth permutation of "123...n" without generating all permutations.
Uses factorial number system for O(n²) solution.
"""
import math
numbers = list(range(1, n + 1))
result = []
k -= 1 # Convert to 0-indexed
for i in range(n, 0, -1):
factorial = math.factorial(i - 1)
index = k // factorial
result.append(str(numbers[index]))
numbers.pop(index)
k %= factorial
return ''.join(result)
🤔 Did you know? The factorial number system is a unique representation where the digit at position i can range from 0 to i. It's perfect for encoding permutations because there are exactly i! permutations starting with the (i+1)th choice.
Putting It All Together
Let's see how these optimizations combine in a complete, interview-ready solution:
public class OptimizedPermutations {
/**
* Generate all unique permutations with multiple optimizations:
* - Pruning for duplicates
* - StringBuilder for efficient string building
* - Early termination
* - Lexicographic ordering
*/
public List<String> permuteOptimized(String s) {
List<String> result = new ArrayList<>();
char[] chars = s.toCharArray();
Arrays.sort(chars); // Enable pruning and lexicographic order
boolean[] used = new boolean[chars.length];
backtrack(chars, used, new StringBuilder(), result);
return result;
}
private void backtrack(char[] chars, boolean[] used,
StringBuilder current, List<String> result) {
// Base case
if (current.length() == chars.length) {
result.add(current.toString());
return;
}
for (int i = 0; i < chars.length; i++) {
// Skip if used
if (used[i]) continue;
// Pruning: skip duplicates
if (i > 0 && chars[i] == chars[i-1] && !used[i-1]) continue;
// Make choice
used[i] = true;
current.append(chars[i]);
// Recurse
backtrack(chars, used, current, result);
// Undo choice (backtrack)
current.deleteCharAt(current.length() - 1);
used[i] = false;
}
}
}
This solution combines multiple optimization patterns:
- 🔧 Pruning eliminates duplicate work
- 🔧 StringBuilder provides O(1) character operations
- 🔧 Sorted input enables both pruning and lexicographic output
- 🔧 Early termination (implicit in pruning conditions)
⚠️ Important: Don't over-optimize prematurely. Start with a correct solution, then apply these patterns based on your specific constraints. A readable O(n!) solution often beats an buggy "optimized" one. ⚠️
With these optimization patterns in your toolkit, you're now equipped to tackle advanced permutation problems efficiently. In the next section, we'll examine common pitfalls and debugging strategies to ensure your solutions are not just fast, but also correct.
Common Pitfalls and Best Practices
After learning the theory and implementation of string permutation algorithms, it's crucial to understand where things commonly go wrong. Even experienced developers stumble over the same issues repeatedly when implementing backtracking solutions. This section will guide you through the most frequent mistakes, helping you write robust, efficient, and correct permutation code.
Pitfall #1: Forgetting to Backtrack
🎯 Key Principle: Backtracking means undoing every state change you make during exploration.
The most common mistake in permutation problems is failing to properly restore state after exploring a branch. When you modify your data structure to explore one possibility, you must undo that modification before exploring the next possibility at the same level.
⚠️ Mistake 1: Incomplete State Restoration ⚠️
def permute_broken(s):
result = []
chars = list(s)
used = [False] * len(chars)
def backtrack(path):
if len(path) == len(chars):
result.append(''.join(path))
return
for i in range(len(chars)):
if not used[i]:
used[i] = True
path.append(chars[i])
backtrack(path)
# ❌ MISTAKE: Forgot to set used[i] = False
path.pop() # Only removed from path, didn't restore used array
backtrack([])
return result
## This produces incomplete results!
print(permute_broken("abc")) # Only gets ['abc'] instead of all 6 permutations
Why this fails: After exploring the first complete permutation, the used array remains [True, True, True], preventing any further exploration. The algorithm thinks all characters are still in use.
✅ Correct Implementation:
def permute_correct(s):
result = []
chars = list(s)
used = [False] * len(chars)
def backtrack(path):
if len(path) == len(chars):
result.append(''.join(path))
return
for i in range(len(chars)):
if not used[i]:
# Make choice
used[i] = True
path.append(chars[i])
# Explore
backtrack(path)
# Undo choice (COMPLETE backtracking)
path.pop()
used[i] = False # ✅ Critical: Restore the used array
backtrack([])
return result
💡 Pro Tip: Think of backtracking as a "try it on, take it off" process. Every piece of state you put on must come off in reverse order.
🧠 Mnemonic: "CLEAN UP YOUR MESS" - After exploring each branch, your data structures should look exactly as they did before you started exploring that branch.
Pitfall #2: Mishandling Duplicate Characters
When your input string contains duplicate characters, naive implementations generate redundant permutations. For example, "aab" should produce ["aab", "aba", "baa"], not six permutations where the two 'a's are treated as distinct.
⚠️ Mistake 2: Treating Duplicates as Unique ⚠️
❌ Wrong thinking: "I'll just generate all permutations and use a Set to remove duplicates at the end."
✅ Correct thinking: "I'll prevent duplicate permutations from being generated in the first place by skipping redundant branches."
The wrong approach wastes computation and memory. For a string with many duplicates, you might generate thousands of permutations only to collapse them down to a handful of unique ones.
The Solution: Sort and Skip Duplicates
def permute_unique(s):
result = []
chars = sorted(list(s)) # ✅ Sort to group duplicates together
used = [False] * len(chars)
def backtrack(path):
if len(path) == len(chars):
result.append(''.join(path))
return
for i in range(len(chars)):
# Skip if already used
if used[i]:
continue
# ✅ KEY: Skip duplicates that haven't had their "turn" yet
# If current char equals previous char AND previous char wasn't used,
# skip current char to avoid duplicate permutations
if i > 0 and chars[i] == chars[i-1] and not used[i-1]:
continue
used[i] = True
path.append(chars[i])
backtrack(path)
path.pop()
used[i] = False
backtrack([])
return result
## Test with duplicates
print(permute_unique("aab")) # ['aab', 'aba', 'baa'] - 3 unique permutations
🎯 Key Principle: The condition if i > 0 and chars[i] == chars[i-1] and not used[i-1] ensures that duplicate characters are used in order. The first 'a' must be used before the second 'a' at any given level of recursion.
💡 Mental Model: Imagine two identical twins standing in line. To avoid duplicate "arrangements," always pick the left twin before the right twin. If the left twin hasn't been picked yet, don't pick the right twin.
Pitfall #3: String Immutability Issues
In languages like Python and Java, strings are immutable. Creating new strings repeatedly during permutation generation leads to massive performance penalties.
⚠️ Mistake 3: Inefficient String Concatenation ⚠️
## ❌ INEFFICIENT: Creates new string objects repeatedly
def permute_slow(s):
result = []
def backtrack(remaining, path):
if not remaining:
result.append(path)
return
for i in range(len(remaining)):
# Each concatenation creates NEW string objects
backtrack(remaining[:i] + remaining[i+1:], path + remaining[i])
backtrack(s, "")
return result
Performance impact: For a 10-character string, this creates thousands of intermediate string objects, each requiring memory allocation and copying.
✅ Best Practice: Use Character Arrays/Lists
## ✅ EFFICIENT: Modifies list in-place
def permute_fast(s):
result = []
chars = list(s) # Convert once to mutable structure
def backtrack(start):
if start == len(chars):
result.append(''.join(chars)) # Convert to string only at end
return
for i in range(start, len(chars)):
chars[start], chars[i] = chars[i], chars[start] # Swap in-place
backtrack(start + 1)
chars[start], chars[i] = chars[i], chars[start] # Swap back
backtrack(0)
return result
🤔 Did you know? The efficient version can be 10-100x faster for longer strings because it avoids memory allocation overhead and garbage collection pressure.
Pitfall #4: Off-by-One Errors
Recursion boundaries are notorious sources of subtle bugs. In permutation problems, these typically manifest in base cases and loop conditions.
⚠️ Mistake 4: Incorrect Base Cases ⚠️
❌ Wrong:
if len(path) > len(chars): # Should be == not >
result.append(''.join(path))
This never triggers because recursion stops before exceeding the length.
❌ Wrong:
if len(path) == len(chars) - 1: # Off by one!
result.append(''.join(path))
This adds incomplete permutations missing the last character.
✅ Correct:
if len(path) == len(chars): # Exact match
result.append(''.join(path))
return
Common Loop Boundary Mistakes:
## Swap-based approach
def backtrack(start):
if start == len(chars): # ✅ Correct: start reaches length
# NOT start == len(chars) - 1
result.append(''.join(chars))
return
for i in range(start, len(chars)): # ✅ Correct: includes start position
# NOT range(start + 1, len(chars))
💡 Pro Tip: Trace through your algorithm with a 1-character string first. If s = "a" doesn't produce exactly ["a"], you have a boundary error.
Pitfall #5: Inadequate Testing
Many developers test only the "happy path" with simple inputs, missing edge cases that reveal bugs.
📋 Quick Reference Card: Essential Test Cases
| 🧪 Test Type | 📝 Example Input | 🎯 What It Tests | ✅ Expected Output |
|---|---|---|---|
| 🔹 Empty string | "" |
Base case handling | [""] or [] depending on spec |
| 🔹 Single character | "a" |
Minimal recursion | ["a"] |
| 🔹 Two characters | "ab" |
Basic swapping | ["ab", "ba"] |
| 🔹 All same | "aaa" |
Duplicate handling | ["aaa"] |
| 🔹 Mixed duplicates | "aab" |
Partial duplicates | ["aab", "aba", "baa"] |
| 🔹 No duplicates | "abc" |
Standard case | 6 permutations |
| 🔹 Performance | "abcdefghij" (10 chars) |
Efficiency | 3,628,800 perms in reasonable time |
Testing Strategy Template:
def test_permutations():
impl = permute_unique # Your implementation
# Edge case: empty
assert impl("") == [""]
# Edge case: single character
assert impl("a") == ["a"]
# Basic case: two characters
result = set(impl("ab"))
assert result == {"ab", "ba"}
# Duplicate handling
result = set(impl("aab"))
assert result == {"aab", "aba", "baa"}
assert len(impl("aab")) == 3 # No redundant permutations
# All same characters
assert impl("aaa") == ["aaa"]
# Count verification (n! for n unique characters)
import math
result = impl("abcd")
assert len(result) == math.factorial(4)
# Performance test
import time
start = time.time()
result = impl("abcdefgh") # 8! = 40,320 permutations
duration = time.time() - start
assert duration < 1.0 # Should complete in under 1 second
print("✅ All tests passed!")
💡 Real-World Example: At Google, a candidate's permutation solution that passed "abc" but failed "aab" (by producing 6 results instead of 3) resulted in interview failure. The duplicate handling bug indicated incomplete understanding.
Best Practices Summary
After navigating these common pitfalls, you now understand the critical distinctions between buggy and robust permutation implementations:
Before this section:
- ❌ You might have left state changes unreverted
- ❌ You might have generated redundant permutations for duplicates
- ❌ You might have used inefficient string operations
- ❌ You might have missed critical edge cases
After this section:
- ✅ You understand complete state restoration is non-negotiable
- ✅ You can prevent duplicate permutations at generation time
- ✅ You know to use mutable character arrays for efficiency
- ✅ You have a comprehensive testing strategy
Critical Points to Remember:
⚠️ Every state change must be undone in reverse order - This is the essence of backtracking.
⚠️ Sort characters and skip unused duplicates - The pattern chars[i] == chars[i-1] and not used[i-1] is crucial.
⚠️ Use character arrays, not string concatenation - Performance matters, especially in interviews.
⚠️ Test edge cases systematically - Empty strings, single characters, and all-duplicates reveal the most bugs.
Practical Applications and Next Steps
Now that you've mastered string permutations and their pitfalls, consider these practical applications:
🔐 Cryptography and Password Generation: Permutation algorithms form the basis of brute-force password crackers and strength analyzers. Understanding efficient implementation helps you estimate attack feasibility and design secure systems.
🎮 Game Development: Procedural content generation often uses permutations to create unique level layouts, character combinations, or puzzle configurations. Your optimized implementations ensure smooth runtime performance.
📊 Data Science and A/B Testing: Permutation tests in statistics require generating all possible arrangements to calculate p-values. Handling duplicates correctly is essential for accurate statistical inference.
Next Steps:
🧠 Practice variations: Try implementing permutations with constraints (e.g., "no two vowels adjacent"), k-length permutations, or permutations of multi-dimensional structures.
📚 Explore related algorithms: Study combinations, subsets, and partition problems - they use similar backtracking patterns but with different constraints.
🔧 Build a permutation library: Create a reusable module with optimized implementations for different scenarios (with/without duplicates, in-place/returning new, memory-optimized/speed-optimized).
By avoiding these common pitfalls and following these best practices, you're now equipped to solve permutation problems confidently in interviews, competitions, and real-world applications. The key is deliberate practice with attention to these details until correct implementation becomes second nature.
ASCII Decision Flow for Debugging:
Permutation Bug?
|
v
Wrong number of results?
/ \
Yes No
| |
v v
Too few? Wrong content?
/ \ |
Yes No v
| | Duplicates?
| v / \
| Too many? Yes No
| | | |
| v v v
| Missing Sort & Check
| backtrack skip string
| dups vs array
v
Check
base case
Mastering these pitfalls transforms you from someone who "mostly gets permutations working" to someone who writes bulletproof, production-ready backtracking code.