1047. Remove All Adjacent Duplicates In String
Problem Description
You have a string s
made up of lowercase English letters. Your task is to repeatedly remove adjacent duplicate characters until no more removals are possible.
A duplicate removal works like this: find two adjacent letters that are the same and remove both of them from the string. After each removal, the remaining parts of the string come together, potentially creating new adjacent duplicates that can be removed.
For example:
- If you have
"abbaca"
, you can remove"bb"
to get"aaca"
, then remove"aa"
to get"ca"
. No more adjacent duplicates exist, so"ca"
is the final answer. - If you have
"azxxzy"
, you can remove"xx"
to get"azzy"
, then remove"zz"
to get"ay"
. No more adjacent duplicates exist, so"ay"
is the final answer.
The process continues until there are no adjacent duplicate pairs left in the string. The problem guarantees that the final result is unique regardless of the order in which you perform the removals.
The solution uses a stack-based approach: iterate through each character in the string, and if the top of the stack matches the current character, pop the stack (removing the duplicate pair). Otherwise, push the current character onto the stack. The final stack contents represent the string after all duplicate removals.
Intuition
When we think about removing adjacent duplicates, we need to consider what happens after each removal - the characters on either side of the removed pair come together and might form a new duplicate pair. This suggests we need a way to efficiently handle these cascading removals.
Consider the string "abbaca"
. If we process it left to right:
- We see
'a'
, nothing to compare with yet - We see
'b'
, different from'a'
, so keep both - We see another
'b'
, same as the previous one - remove both - Now we have
'a'
and need to process'a'
next - These two
'a'
s are now adjacent - remove both - Continue with
'c'
and'a'
This process naturally follows a Last-In-First-Out (LIFO) pattern. When we encounter a character, we need to check it against the most recently kept character. If they match, we remove both (the current one and the most recent kept one). If they don't match, we keep the current character as the new "most recent."
A stack is the perfect data structure for this LIFO behavior. As we traverse the string:
- If the stack is empty or the top element doesn't match the current character, we push the current character
- If the top element matches the current character, we pop the stack (removing the duplicate) and don't add the current character
This way, the stack always maintains the characters that haven't been removed yet, and adjacent duplicates are automatically eliminated as we process each character. The final stack contents give us the string after all possible removals.
Learn more about Stack patterns.
Solution Approach
The implementation uses a stack to efficiently handle the duplicate removal process. Here's how the algorithm works step by step:
-
Initialize an empty stack:
stk = []
- This will store the characters that remain after removing duplicates. -
Iterate through each character in the string:
for c in s:
- Process the string from left to right, one character at a time. -
Check for duplicate pairs:
if stk and stk[-1] == c:
- Check if the stack is not empty AND the top element (last element in the list) equals the current character- If true:
stk.pop()
- Remove the top element from the stack (this simulates removing both the current character and its duplicate) - If false:
stk.append(c)
- Add the current character to the stack (no duplicate found)
-
Build the final result:
return ''.join(stk)
- Convert the stack (list of characters) back into a string.
Example walkthrough with "abbaca"
:
- Process
'a'
: Stack is empty, push'a'
→ Stack:['a']
- Process
'b'
: Top is'a'
≠'b'
, push'b'
→ Stack:['a', 'b']
- Process
'b'
: Top is'b'
='b'
, pop stack → Stack:['a']
- Process
'a'
: Top is'a'
='a'
, pop stack → Stack:[]
- Process
'c'
: Stack is empty, push'c'
→ Stack:['c']
- Process
'a'
: Top is'c'
≠'a'
, push'a'
→ Stack:['c', 'a']
- Result:
"ca"
The time complexity is O(n)
where n
is the length of the string, as we process each character once. The space complexity is also O(n)
in the worst case when no duplicates are removed.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with the string "azxxzy"
to see how the stack-based approach removes all adjacent duplicates.
Initial state:
- Input string:
"azxxzy"
- Stack:
[]
(empty)
Step-by-step process:
-
Process 'a':
- Stack is empty, so push 'a'
- Stack:
['a']
-
Process 'z':
- Top of stack is 'a' ≠ 'z', so push 'z'
- Stack:
['a', 'z']
-
Process first 'x':
- Top of stack is 'z' ≠ 'x', so push 'x'
- Stack:
['a', 'z', 'x']
-
Process second 'x':
- Top of stack is 'x' = 'x', so pop the stack (removing the duplicate pair)
- Stack:
['a', 'z']
-
Process 'z':
- Top of stack is 'z' = 'z', so pop the stack (removing this duplicate pair)
- Stack:
['a']
-
Process 'y':
- Top of stack is 'a' ≠ 'y', so push 'y'
- Stack:
['a', 'y']
Final result: Join the stack elements to get "ay"
Notice how when we removed the 'xx' pair in step 4, it brought the two 'z' characters together, which we then removed in step 5. This cascading effect is exactly what the stack handles automatically - we don't need to restart or look back, the stack maintains the correct state throughout the process.
Solution Implementation
1class Solution:
2 def removeDuplicates(self, s: str) -> str:
3 """
4 Remove all adjacent duplicates from a string.
5
6 Args:
7 s: Input string to process
8
9 Returns:
10 String with all adjacent duplicates removed
11 """
12 # Use a stack to track characters
13 stack = []
14
15 # Iterate through each character in the string
16 for char in s:
17 # If stack is not empty and top element equals current character
18 if stack and stack[-1] == char:
19 # Remove the duplicate by popping from stack
20 stack.pop()
21 else:
22 # Add the character to stack if no duplicate found
23 stack.append(char)
24
25 # Join all remaining characters in stack to form the result
26 return ''.join(stack)
27
1class Solution {
2 /**
3 * Removes all adjacent duplicate characters from the string.
4 * When two adjacent characters are the same, both are removed.
5 * This process continues until no adjacent duplicates remain.
6 *
7 * @param s the input string to process
8 * @return the string after removing all adjacent duplicates
9 */
10 public String removeDuplicates(String s) {
11 // Use StringBuilder as a stack to build the result
12 StringBuilder resultBuilder = new StringBuilder();
13
14 // Process each character in the input string
15 for (char currentChar : s.toCharArray()) {
16 // Check if the last character in result matches current character
17 if (resultBuilder.length() > 0 &&
18 resultBuilder.charAt(resultBuilder.length() - 1) == currentChar) {
19 // Remove the last character (pop from stack)
20 resultBuilder.deleteCharAt(resultBuilder.length() - 1);
21 } else {
22 // Add current character to result (push to stack)
23 resultBuilder.append(currentChar);
24 }
25 }
26
27 // Convert StringBuilder to String and return
28 return resultBuilder.toString();
29 }
30}
31
1class Solution {
2public:
3 string removeDuplicates(string s) {
4 // Use a string as a stack to store characters
5 string stack;
6
7 // Iterate through each character in the input string
8 for (char currentChar : s) {
9 // Check if stack is not empty and top element equals current character
10 if (!stack.empty() && stack.back() == currentChar) {
11 // Remove the top element (adjacent duplicate found)
12 stack.pop_back();
13 } else {
14 // Add current character to the stack
15 stack.push_back(currentChar);
16 }
17 }
18
19 // Return the final string with all adjacent duplicates removed
20 return stack;
21 }
22};
23
1/**
2 * Removes all adjacent duplicate characters from a string
3 * @param s - The input string to process
4 * @returns A string with all adjacent duplicates removed
5 */
6function removeDuplicates(s: string): string {
7 // Stack to track characters, automatically handling adjacent duplicates
8 const stack: string[] = [];
9
10 // Iterate through each character in the input string
11 for (const char of s) {
12 // Check if the top of stack matches current character
13 if (stack.length > 0 && stack[stack.length - 1] === char) {
14 // Remove the duplicate from stack
15 stack.pop();
16 } else {
17 // Add non-duplicate character to stack
18 stack.push(char);
19 }
20 }
21
22 // Convert stack to string and return the result
23 return stack.join('');
24}
25
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the input string s
. The algorithm iterates through each character in the string exactly once. Each operation inside the loop (checking the top of the stack, pushing, and popping) takes O(1)
time. The final join
operation also takes O(n)
time to concatenate all remaining characters in the stack. Therefore, the overall time complexity is O(n) + O(n) = O(n)
.
Space Complexity: O(n)
, where n
is the length of the input string s
. In the worst case scenario, when no adjacent duplicates exist in the string (e.g., "abcdef"), all characters will be pushed onto the stack, requiring O(n)
space. The output string created by join
also requires O(n)
space in the worst case, but since we typically only count auxiliary space and the output is required, the space complexity remains O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Attempting String Concatenation Instead of Using a Stack
A common mistake is trying to build the result string directly through concatenation, which leads to inefficient code and complexity in handling removals:
Incorrect Approach:
def removeDuplicates(self, s: str) -> str:
result = ""
for char in s:
if result and result[-1] == char:
result = result[:-1] # String slicing creates a new string
else:
result += char # String concatenation creates a new string
return result
Why it's problematic:
- Strings are immutable in Python, so each concatenation or slicing operation creates a new string object
- This leads to O(n²) time complexity instead of O(n)
- Memory usage becomes inefficient due to multiple string copies
Solution: Use a list (stack) to collect characters and join them at the end, as shown in the correct implementation.
2. Forgetting to Check if Stack is Empty
Incorrect Code:
def removeDuplicates(self, s: str) -> str:
stack = []
for char in s:
if stack[-1] == char: # ERROR: Will crash when stack is empty
stack.pop()
else:
stack.append(char)
return ''.join(stack)
Why it fails:
- Accessing
stack[-1]
when the stack is empty raises anIndexError
- This happens at the start of iteration or after removing pairs that empty the stack
Solution: Always check if stack and stack[-1] == char:
to ensure the stack has elements before accessing the top.
3. Misunderstanding the Problem - Removing Only First Occurrence
Some might incorrectly think they should remove duplicates only once without considering chain reactions:
Incorrect Logic:
def removeDuplicates(self, s: str) -> str:
i = 0
while i < len(s) - 1:
if s[i] == s[i + 1]:
s = s[:i] + s[i+2:] # Remove the pair
# Incorrect: just continue without reconsidering position
continue
i += 1
return s
Why it's wrong:
- After removing a pair, new adjacent duplicates might form
- The algorithm must reconsider the position after each removal
- Using string slicing for in-place modification is inefficient
Solution: The stack approach naturally handles chain reactions - when we pop an element, the next comparison automatically happens with the newly exposed top element.
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!