Given a string, continually perform the following operation: remove a pair of adjacent duplicates from the string. Continue performing this operation until the string no longer contains pairs of adjacent duplicates. Return the final string.

Example 1:

Image represents a sequence of data transformations or operations.  The sequence begins with two 'a's connected by a red horizontal line, followed by a 'c'. This is then followed by an 'a' and two 'b's connected by a red horizontal line, followed by another 'a'. A grey arrow indicates a transformation from this first segment to a second segment. The second segment starts with a 'c', followed by two 'a's connected by a red horizontal line, and finally another 'c'. A grey arrow indicates the flow from the first segment to the second, suggesting a transformation or mapping from the input (the first segment) to the output (the second segment). The red lines visually highlight groups of elements undergoing a specific operation or transformation, while the grey arrows represent the overall transformation process between the input and output.  The letters 'a', 'b', and 'c' likely represent data elements or variables.
Input: s = 'aacabba'
Output: 'c'

Example 2:

Image represents a simple data flow diagram illustrating a data transformation or processing step.  The diagram shows two instances of the variable 'a' connected by a thick red horizontal line, suggesting a direct, possibly in-place, modification or operation on 'a'. This is followed by a grey arrow pointing to another instance of 'a', indicating that the modified 'a' (from the red line connection) is then passed on or transformed into a new, potentially different, 'a'. The absence of labels on the connections or the 'a' variables themselves prevents a more precise description of the specific operation or transformation involved, but the visual structure clearly shows a sequential process where data ('a') undergoes a transformation and is then passed along.
Input: s = 'aaa'
Output: 'a'

In Plain English

New to this one? Start here. Below is the core idea in everyday language, plus a real-world analogy—then the sections that follow build on it with the formal reasoning, code, and step-by-step walkthroughs.

🌱 A real-world picture: a match-3 puzzle

Think of coloured tiles in a row. Whenever two identical tiles sit next to each other, they pop and vanish, and the tiles on either side slide together. That slide can line up a new matching pair, which pops too — a satisfying chain reaction. We’re computing what’s left once all the chains finish.

Intuition

One challenge in solving this problem is how we handle characters which aren’t currently adjacent duplicates but will be in the future.

A solution we can try is to iteratively build the string character by character and immediately remove each pair of adjacent duplicates that get formed as we’re building the string.

It’s also possible an adjacent duplicate may be formed after another adjacent duplicate gets removed. For example, with the string “abba”, removing “bb” will result in “aa”. Building the string character by character ensures the formation of “aa” gets noticed and removed. To better understand how this works, let’s dive into an example.

Consider the following string:

Image represents a sequence of lowercase letters arranged horizontally.  The sequence begins with two instances of the letter 'a', followed by a single 'c', then another 'a', and then two 'b's, concluding with a final 'a'.  There are no connections or arrows depicted between the letters; they are simply presented in a linear, left-to-right order. No URLs, parameters, or other additional information is present beyond the letters themselves.  The letters are spaced evenly apart, suggesting an ordered list or a simple data stream.

Image represents a visual depiction of an algorithm, likely for string manipulation.  The top section shows an input string 'a a c a b b a' preceded by a downward-pointing arrow originating from a square box labeled 'i,' suggesting an input indicator.  This input string is then processed, resulting in a 'new string: a' displayed to the right.  The bottom section mirrors the top, again showing the input string 'a a c a b b a' with the same input indicator 'i.'  However, the resulting 'new string' is shown as 'a [a]' with a red line striking through the second 'a', indicating removal.  A dashed-line box to the right of the bottom section labels this action as 'remove adjacent duplicate,' clarifying the algorithmic step performed.  The overall diagram illustrates a process where adjacent duplicate characters are identified and removed from the input string.

At the second ‘a’, we notice that adding it would result in an adjacent duplicate forming (i.e., “aa”). So, let’s remove this duplicate before adding any new characters. We’ll do this for all adjacent duplicates we come across as we build the string:

Image represents a step-by-step illustration of an algorithm to remove adjacent duplicate characters from a string.  The initial string 'a a c a b b a' is displayed at the top.  A downward-pointing arrow labeled 'i' indicates an iterative process.  Each subsequent line shows the string after one iteration, with a new character being added to a 'new string' on the right.  The 'new string' starts empty and progressively accumulates characters from the original string.  Crucially, adjacent duplicate characters in the original string are skipped in the 'new string' construction.  The process continues until the index 'i' reaches the end of the original string.  Two rectangular boxes labeled 'remove adjacent duplicate' are placed to the right, visually associating this action with the iterative removal of adjacent duplicates.  The final 'new string' is 'c a', representing the string with adjacent duplicates removed.  The boxed characters in the 'new string' highlight the character added in each iteration.

Once the smoke clears, the resulting string we were building ends up just being “c”, which is the expected output.


Now that we know how this strategy works, we just need a data structure that'll allow us to:

  1. Add letters to one end of it.
  2. Remove letters from the same end.

The stack data structure is a strong option because it allows for both operations.

As we push characters onto the stack, the top of the stack will represent the previous/most recently added character. Given this, to mimic the process of building the “new string” as shown in the example, we:

Once all characters have been processed, the last thing to do is return the content of the stack as a string, since the final state of the stack will contain all characters that weren’t removed.

Implementation

def repeated_removal_of_adjacent_duplicates(s: str) -> str:
    stack = []
    for c in s:
        # If the current character is the same as the top character on the stack,
        # a pair of adjacent duplicates has been formed. So, pop the top character
        # from the stack.
        if stack and c == stack[-1]:
            stack.pop()
        # Otherwise, push the current character onto the stack.
        else:
            stack.append(c)
    # Return the remaining characters as a string.
    return ''.join(stack)
string repeatedRemovalOfAdjacentDuplicates(string s) {
    string stack;                            // a string works perfectly as a stack
    for (char c : s) {
        if (!stack.empty() && stack.back() == c) {
            stack.pop_back();                // a matching pair cancels out
        } else {
            stack.push_back(c);
        }
    }
    return stack;
}
public String repeatedRemovalOfAdjacentDuplicates(String s) {
    StringBuilder stack = new StringBuilder();
    for (char c : s.toCharArray()) {
        int len = stack.length();
        if (len > 0 && stack.charAt(len - 1) == c) stack.deleteCharAt(len - 1);
        else stack.append(c);
    }
    return stack.toString();
}

Complexity Analysis

Time complexity: The time complexity of the repeated_removal_of_adjacent_duplicates function is O(n)O(n) where nn denotes the length of the string. This is because we traverse the entire string, and we perform a join operation of up to nn characters in the stack. The stack push and pop operations contribute O(1)O(1) time.

Space complexity: The space complexity is O(n)O(n) because the stack can store at most nn characters.

Dry Runs

Two step-by-step walkthroughs of the algorithm below: a valid case (the expected happy path) and an invalid / edge case that exercises the tricky parts. Each step shows the program state as the code runs—the same steps apply to Python, C++ and Java.

✅ Dry run 1 (partial, with a chain) — s = "abbaca""ca". Popping bb exposes an a that then cancels the next a.

pushed / top cancelled (popped)

Reading a b b a c a (indices 0–5).

i=0 'a'. Stack empty → push.
atop
i=1 'b'. Top is ab → push.
a
btop
i=2 'b'. Top is b = b → pop! Both bs vanish.
a
b ✗
i=3 'a'. New top is a (just exposed) = a → pop! Chain reaction.
a ✗
i=4 'c'. Stack empty → push.
ctop
i=5 'a'. Top is ca → push.
c
atop
End. Stack is c a → return "ca". 🎉

The same run as a table

Popping the bb pair at i=2 is what lets the a at i=3 find its twin.
ichartop beforeactionstack after
0apusha
1bapusha b
2bbmatch → popa
3aamatch → pop(empty)
4cpushc
5acpushc a
join stackreturn "ca"
❌ Dry run 2 (full annihilation) — s = "abccba""". A palindrome of pairs cancels all the way down to nothing.

pushed / top cancelled (popped)

Reading a b c c b a (indices 0–5).

i=0..2 'a','b','c' all differ from the top → push each.
a
b
ctop
i=3 'c'. Top c = c → pop. Exposes b.
a
btop
c ✗
i=4 'b'. Top b = b → pop. Exposes a.
atop
b ✗
i=5 'a'. Top a = a → pop. Stack is now empty.
a ✗
End. Nothing is left → return "" (empty string). 🎉

The same run as a table

Each closer half cancels its matching opener half — like brackets, but for equal letters.
ichartop beforeactionstack after
0apusha
1bapusha b
2cbpusha b c
3ccmatch → popa b
4bbmatch → popa
5aamatch → pop(empty)""

Worst case for the brute force (it re-scans after each of the three deletions), but the stack still touches each character once.

The same steps in each language

A list is the stack; stack[-1] is the top, stack.pop() cancels, and "".join(stack) builds the answer at the end.

A std::string doubles as the stack: back() is the top, pop_back() cancels, and we simply return it.

A StringBuilder is the stack: charAt(len-1) is the top and deleteCharAt(len-1) cancels — both O(1) at the end.