Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Initialize an empty stack: stk = [] - This will store the characters that remain after removing duplicates.

  2. Iterate through each character in the string: for c in s: - Process the string from left to right, one character at a time.

  3. 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)
  4. 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 Evaluator

Example 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:

  1. Process 'a':

    • Stack is empty, so push 'a'
    • Stack: ['a']
  2. Process 'z':

    • Top of stack is 'a' ≠ 'z', so push 'z'
    • Stack: ['a', 'z']
  3. Process first 'x':

    • Top of stack is 'z' ≠ 'x', so push 'x'
    • Stack: ['a', 'z', 'x']
  4. Process second 'x':

    • Top of stack is 'x' = 'x', so pop the stack (removing the duplicate pair)
    • Stack: ['a', 'z']
  5. Process 'z':

    • Top of stack is 'z' = 'z', so pop the stack (removing this duplicate pair)
    • Stack: ['a']
  6. 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 an IndexError
  • 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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More