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.
Input: s = 'aacabba'
Output: 'c'
Input: s = 'aaa'
Output: 'a'
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.
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.
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:
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:
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:
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:
Push the current character onto the stack if it’s different from the character at the top (i.e., not a duplicate character.)
Pop off the character at the top of the stack if it's the same as the current character (i.e., a duplicate.)
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.
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();
}Time complexity: The time complexity of the repeated_removal_of_adjacent_duplicates function is where denotes the length of the string. This is because we traverse the entire string, and we perform a join operation of up to characters in the stack. The stack push and pop operations contribute time.
Space complexity: The space complexity is because the stack can store at most characters.
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.
"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).
a ≠ b → push.
b = b → pop! Both bs vanish.
a (just exposed) = a → pop! Chain reaction.
c ≠ a → push.
c a → return "ca". 🎉
| i | char | top before | action | stack after |
|---|---|---|---|---|
| 0 | a | — | push | a |
| 1 | b | a | push | a b |
| 2 | b | b | match → pop | a |
| 3 | a | a | match → pop | (empty) |
| 4 | c | — | push | c |
| 5 | a | c | push | c a |
| — | — | — | join stack | return "ca" |
"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).
c = c → pop. Exposes b.
b = b → pop. Exposes a.
a = a → pop. Stack is now empty.
| i | char | top before | action | stack after |
|---|---|---|---|---|
| 0 | a | — | push | a |
| 1 | b | a | push | a b |
| 2 | c | b | push | a b c |
| 3 | c | c | match → pop | a b |
| 4 | b | b | match → pop | a |
| 5 | a | a | match → 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.
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.