Skip to content

1047. Remove All Adjacent Duplicates In String (Easy)

Problem

Given a string s, repeatedly remove all adjacent duplicate pairs until no more adjacent duplicates exist. Return the final string.

Adjacent duplicate removal is applied to the entire string simultaneously on each pass (like popping bubbles). The order of removal does not matter: the result is always unique.

Examples

  • "abbaca""ca"
    • Remove bb: "aaca"
    • Remove aa: "ca"
  • "azxxzy""ay"
    • Remove xx: "azzy"
    • Remove zz: "ay"

LeetCode 1047 · Link · Easy

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Repeated scanning

Scan the string repeatedly, removing any adjacent duplicate pair on each pass. Stop when no pair is found.

def remove_duplicates(s: str) -> str:
changed = True
while changed: # L1: up to n/2 passes
changed = False
result = []
i = 0
while i < len(s): # L2: O(n) per pass
if i + 1 < len(s) and s[i] == s[i + 1]: # L3: O(1) compare
i += 2 # L4: skip the pair
changed = True
else:
result.append(s[i])
i += 1
s = "".join(result) # L5: O(n) join
return s

Complexity

  • Time: O(n2)O(n²), up to n/2 passes each taking O(n)O(n).
  • Space: O(n)O(n) for result buffer.

Approach 2: Stack cancel (optimal)

Process each character once. If the stack is non-empty and the top equals the current character, they cancel (pop). Otherwise push. The stack accumulates the surviving characters in order.

def remove_duplicates(s: str) -> str:
stack = [] # L1: O(1) init
for ch in s: # L2: n iterations
if stack and stack[-1] == ch: # L3: O(1) compare top
stack.pop() # L4: O(1) cancel pair
else:
stack.append(ch) # L5: O(1) no match, push
return "".join(stack) # L6: O(n) join survivors

Where the time goes, line by line

Variables: n = len(s).

LinePer-call costTimes executedContribution
L2 (loop)O(1)O(1)nO(n)O(n)
L3-L5 (compare + push/pop)O(1)O(1)nO(n)O(n) ← dominates
L6 (join)O(n)O(n)1O(n)O(n)

Each character is pushed once and popped at most once. Total: O(n)O(n).

Complexity

  • Time: O(n)O(n), one pass with O(1)O(1) per character.
  • Space: O(n)O(n) for the stack (at most n characters survive).

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Why one pass is enough

The multi-pass intuition is that removing one pair might expose a new adjacent pair. The stack handles this automatically: after a pop, the new top of the stack is now adjacent to the next character to process. If they match, the next iteration catches it immediately.

s = "azxxzy"
Push a: stack = ['a']
Push z: stack = ['a','z']
Push x: stack = ['a','z','x']
Push x: top='x' == 'x', pop. stack = ['a','z']
Push z: top='z' == 'z', pop. stack = ['a']
Push y: stack = ['a','y']
Result: "ay"

The two z characters only became adjacent after the xx pair was removed. The stack handles this in the same pass because popping xx immediately puts z at the top.

Key takeaways

  • The stack simulates all cascading removals in a single pass. Each pop is equivalent to one full pass of the naive approach.
  • The pattern (push; if top equals current, pop instead) appears in several string cancellation problems.
  • "".join(stack) at the end is O(n)O(n); calling it inside the loop would make the approach quadratic.
  • This is the building block for the harder variant (1209. Remove All Adjacent Duplicates in String II), where k-length runs are removed.
  • Stack Parsing, the last open, first closed model for nested syntax and reversible operations.
  • Simulation, the explicit state model for executing rules exactly while keeping cases organized.