[Easy] 1047. Remove All Adjacent Duplicates In String
[Easy] 1047. Remove All Adjacent Duplicates In String
You are given a string s consisting of lowercase English letters. A duplicate removal consists of choosing two adjacent and equal letters and removing them.
We repeatedly make duplicate removals on s until we no longer can.
Return the final string after all such duplicate removals have been made. It can be proven that the answer is unique.
Examples
Example 1:
Input: s = "abbaca"
Output: "ca"
Explanation:
For example, in "abbaca" we could remove "bb" since the letters are adjacent and equal, and this is the only possible move. The result of this move is "aaca" of which only "aa" is possible, so the final string is "ca".
Example 2:
Input: s = "azxxzy"
Output: "ay"
Explanation:
First, we remove "xx" to get "azzy". Then we remove "zz" to get "ay".
Constraints
1 <= s.length <= 10^5sconsists of lowercase English letters.
Clarification Questions
Before diving into the solution, here are 5 important clarifications and assumptions to discuss during an interview:
-
Duplicate removal: What does “adjacent duplicates” mean? (Assumption: Two identical characters next to each other - “aa” is adjacent duplicates)
-
Removal process: How should we remove duplicates? (Assumption: Remove pairs of adjacent duplicates repeatedly until no more exist)
-
Cascading removal: Can removal cause new duplicates? (Assumption: Yes - removing “aa” from “baa” leaves “b”, removing “aa” from “aab” leaves “b”)
-
Return format: What should we return? (Assumption: Final string after all duplicate removals)
-
Empty string: What if string becomes empty? (Assumption: Return empty string “”)
Interview Deduction Process (10 minutes)
Step 1: Brute-Force Approach (2 minutes)
Initial Thought: “I need to remove adjacent duplicates. Let me scan and remove pairs repeatedly.”
Naive Solution: Repeatedly scan string, remove adjacent duplicate pairs, repeat until no more duplicates.
Complexity: O(n²) worst case, O(n) space
Issues:
- O(n²) time - multiple passes
- String manipulation is expensive
- Doesn’t leverage stack structure
- Can be optimized
Step 2: Semi-Optimized Approach (3 minutes)
Insight: “I can use stack to track characters. When duplicate found, pop from stack.”
Improved Solution: Use stack. For each character, if matches stack top, pop; otherwise push. Stack naturally handles cascading removals.
Complexity: O(n) time, O(n) space
Improvements:
- Stack handles cascading removals naturally
- O(n) time - single pass
- Clean and intuitive
- Can optimize space
Step 3: Optimized Solution (5 minutes)
Final Optimization: “Can use string as stack or two pointers to optimize space.”
Best Solution: Stack approach is optimal. Can use string as stack (modify in-place) or two pointers to simulate stack, reducing space.
Complexity: O(n) time, O(n) space (can optimize to O(1) with two pointers)
Key Realizations:
- Stack is perfect for matching/removal problems
- O(n) time is optimal - single pass
- Stack handles cascading removals elegantly
- Space can be optimized with two pointers
Solution Approaches
Approach 1: Stack-Based Solution
Time Complexity: O(n)
Space Complexity: O(n)
Use a stack (or string as stack) to track characters. When encountering a character that matches the top of the stack, pop it. Otherwise, push the character.
Approach 2: In-Place Two Pointers
Time Complexity: O(n)
Space Complexity: O(1) excluding output space
Use two pointers to simulate a stack in-place. left acts as the stack pointer, right iterates through the string.
Solution 1: Stack-Based (String as Stack)
class Solution {
public:
string removeDuplicates(string s) {
string stk;
for(char ch: s) {
if(!stk.empty() && stk.back() == ch) {
stk.pop_back();
} else {
stk.push_back(ch);
}
}
return stk;
}
};
Solution 2: In-Place Two Pointers
class Solution {
public:
string removeDuplicates(string s) {
int left = -1;
for(int right = 0; right < s.size(); right++) {
if(left >= 0 && s[right] == s[left]) {
left--;
continue;
}
s[++left] = s[right];
}
return s.substr(0, left + 1);
}
};
How the Algorithms Work
Stack-Based Approach
Key Insight: This problem is similar to matching parentheses. When we see a duplicate, we remove both characters (like popping from stack).
Step-by-Step Example: s = "abbaca"
Step | Char | Stack Before | Action | Stack After
-----|------|--------------|--------|-------------
0 | - | "" | Init | ""
1 | 'a' | "" | Push | "a"
2 | 'b' | "a" | Push | "ab"
3 | 'b' | "ab" | Pop | "a" (b == b)
4 | 'a' | "a" | Pop | "" (a == a)
5 | 'c' | "" | Push | "c"
6 | 'a' | "c" | Push | "ca"
Result: "ca"
Visual Representation:
"abbaca"
↓
"a" → push 'a'
"ab" → push 'b'
"a" → pop 'b' (duplicate)
"" → pop 'a' (duplicate)
"c" → push 'c'
"ca" → push 'a'
In-Place Two Pointers Approach
Key Insight: Use left as a stack pointer. When we find a duplicate, decrement left (simulating pop). Otherwise, increment left and assign (simulating push).
Step-by-Step Example: s = "abbaca"
Step | right | s[right] | left | s[0..left] | Action
-----|-------|----------|------|-------------|--------
0 | 0 | 'a' | -1 | "" | left=0, s[0]='a'
1 | 1 | 'b' | 0 | "a" | left=1, s[1]='b'
2 | 2 | 'b' | 1 | "ab" | left=0 (pop: b==b)
3 | 3 | 'a' | 0 | "a" | left=-1 (pop: a==a)
4 | 4 | 'c' | -1 | "" | left=0, s[0]='c'
5 | 5 | 'a' | 0 | "c" | left=1, s[1]='a'
Result: s.substr(0, 2) = "ca"
Visual Representation:
Initial: left = -1, right = 0
s = ['a', 'b', 'b', 'a', 'c', 'a']
↑
right=0, left=-1 → left=0, s[0]='a'
s = ['a', 'b', 'b', 'a', 'c', 'a']
↑
right=1, left=0 → left=1, s[1]='b'
s = ['a', 'b', 'b', 'a', 'c', 'a']
↑
right=2, left=1, s[2]=='b'==s[1] → left=0 (pop)
s = ['a', 'b', 'b', 'a', 'c', 'a']
↑
right=3, left=0, s[3]=='a'==s[0] → left=-1 (pop)
s = ['a', 'b', 'b', 'a', 'c', 'a']
↑
right=4, left=-1 → left=0, s[0]='c'
s = ['a', 'b', 'b', 'a', 'c', 'a']
↑
right=5, left=0 → left=1, s[1]='a'
Final: s.substr(0, 2) = "ca"
Key Insights
- Stack Pattern: This is essentially a stack problem - matching adjacent pairs
- In-Place Optimization: Can simulate stack using two pointers to save space
- Greedy Approach: Remove duplicates as soon as we find them
- No Need for Multiple Passes: Single pass is sufficient with proper data structure
Algorithm Breakdown
Stack-Based Solution
string stk;
for(char ch: s) {
if(!stk.empty() && stk.back() == ch) {
stk.pop_back(); // Remove duplicate
} else {
stk.push_back(ch); // Add character
}
}
return stk;
How it works:
- Iterate through each character
- If stack is not empty and top matches current character → pop (remove duplicate)
- Otherwise → push current character
- Return final stack contents
In-Place Two Pointers Solution
int left = -1; // Stack pointer (points to last valid character)
for(int right = 0; right < s.size(); right++) {
if(left >= 0 && s[right] == s[left]) {
left--; // Pop: move stack pointer back
continue;
}
s[++left] = s[right]; // Push: increment and assign
}
return s.substr(0, left + 1);
How it works:
leftacts as stack pointer (points to last valid character)rightiterates through input string- If
left >= 0ands[right] == s[left]→ decrementleft(pop) - Otherwise → increment
leftand assigns[right](push) - Return substring from 0 to
left+1
Edge Cases
- Empty string: Returns empty string
- All duplicates:
"aaaa"→"" - No duplicates:
"abc"→"abc" - Nested duplicates:
"abccba"→"" - Single character:
"a"→"a"
Complexity Analysis
| Approach | Time | Space | Pros | Cons |
|---|---|---|---|---|
| Stack (String) | O(n) | O(n) | Simple, readable | Extra space |
| In-Place Two Pointers | O(n) | O(1) | Space efficient | Slightly more complex |
Implementation Details
Stack-Based: Why String Works
string stk; // Acts as stack
stk.back() // Top of stack
stk.pop_back() // Pop from stack
stk.push_back() // Push to stack
**Why use string instead of stack
- Easier to return result (no need to reverse)
- Same time complexity
- More memory efficient for this use case
In-Place: Two Pointer Logic
int left = -1; // Points to last valid character (-1 means empty)
Why left = -1 initially?
- Represents empty stack
left >= 0check ensures stack is not empty before comparings[++left]increments first, then assigns (pushes to stack)
Why continue after decrementing?
- Skip assigning current character (we’ve already “popped” it)
- Move to next character immediately
Alternative Approaches
Approach 3: Using STL Stack
Time Complexity: O(n)
Space Complexity: O(n)
class Solution {
public:
string removeDuplicates(string s) {
stack<char> stk;
for(char ch: s) {
if(!stk.empty() && stk.top() == ch) {
stk.pop();
} else {
stk.push(ch);
}
}
string result;
while(!stk.empty()) {
result = stk.top() + result;
stk.pop();
}
return result;
}
};
Pros:
- Explicit stack usage
- Clear intent
Cons:
- Need to reverse result
- More verbose
Approach 4: Recursive Solution
Time Complexity: O(n²) worst case
Space Complexity: O(n) for recursion stack
class Solution {
public:
string removeDuplicates(string s) {
for(int i = 0; i < (int)s.size() - 1; i++) {
if(s[i] == s[i+1]) {
return removeDuplicates(s.substr(0, i) + s.substr(i+2));
}
}
return s;
}
};
Pros:
- Intuitive recursive thinking
Cons:
- Inefficient (creates new strings)
- Stack overflow risk for large inputs
Common Mistakes
- Not handling empty stack: Forgetting to check
!stk.empty()before accessing top - Wrong comparison: Comparing with wrong character
- In-place index errors: Off-by-one errors with
leftpointer - Multiple passes: Trying to do multiple passes instead of single pass
- Not using continue: In in-place solution, forgetting
continueafter decrementing
Optimization Tips
- Use string as stack: More efficient than
stack<char>for this problem - In-place when possible: Two-pointer approach saves space
- Early termination: Can optimize if we know string length (not applicable here)
Related Problems
- 20. Valid Parentheses - Similar stack pattern
- 1544. Make The String Great - Similar duplicate removal
- 1209. Remove All Adjacent Duplicates in String II - Extension with k duplicates
- 1047. Remove All Adjacent Duplicates In String - This problem
Real-World Applications
- Text Processing: Removing duplicate characters in text editors
- Data Cleaning: Removing adjacent duplicates in data streams
- Compression: Basic run-length encoding preprocessing
- Parsing: Removing redundant tokens in parsers
Pattern Recognition
This problem demonstrates the “Stack for Matching Pairs” pattern:
1. Use stack to track elements
2. When encountering matching pair → pop
3. Otherwise → push
4. Return remaining stack contents
Similar problems:
- Valid Parentheses
- Make The String Great
- Remove K Digits
- Decode String
Why Stack Works
- LIFO Property: Last character added is first to be matched
- Adjacent Matching: Duplicates are always adjacent, matching stack’s top
- Cascading Removals: Removing one pair may create new adjacent pairs
- Single Pass: Stack handles cascading removals in one pass
In-Place Optimization Explanation
Why it works:
leftpointer simulates stack tops[0..left]represents current stack contents- When duplicate found, decrement
left(pop) - When new character, increment
leftand assign (push) - Final result is
s[0..left]
Space savings:
- Stack approach: O(n) extra space
- In-place: O(1) extra space (reusing input string)
Step-by-Step Trace: s = "azxxzy"
Stack Approach:
'a' → push → stack: "a"
'z' → push → stack: "az"
'x' → push → stack: "azx"
'x' → pop → stack: "az" (x == x)
'z' → pop → stack: "a" (z == z)
'y' → push → stack: "ay"
Result: "ay"
In-Place Approach:
right=0: 'a' → left=0, s[0]='a'
right=1: 'z' → left=1, s[1]='z'
right=2: 'x' → left=2, s[2]='x'
right=3: 'x' → left=1 (pop, x==x)
right=4: 'z' → left=0 (pop, z==z)
right=5: 'y' → left=1, s[1]='y'
Result: s.substr(0, 2) = "ay"
Comparison: Stack vs In-Place
| Aspect | Stack | In-Place |
|---|---|---|
| Readability | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Space | O(n) | O(1) |
| Time | O(n) | O(n) |
| Code Length | Shorter | Slightly longer |
| Best For | Clarity | Space constraints |
This problem is an excellent example of using a stack to solve matching problems, with an elegant in-place optimization using two pointers.