DSA sheet · Stack · Simulation & undo pattern

Remove All Adjacent Duplicates In String

The second problem of the simulation and undo pattern. Two equal letters standing side by side cancel each other, and after they vanish, the letters around them become neighbours and may cancel too. The teacher first writes a brute force that keeps scanning the string again and again until nothing changes, shows why it is O(n²) in the worst case (TLE for n = 10⁵), then switches to a stack for O(n). Finally she makes an interview point: the same stack idea written with a string builder runs faster, and she explains why.

Why it matters: this "compare with the top, cancel or push" loop is the template for the next two problems (Make The String Great, Min Length After Removing Substrings). Learn it once here.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a stack?

A stack is a pile where you only touch the top, like a pile of plates. The last plate you put on is the first one you take off: LIFO, Last In First Out.

operationmeaningPython (list as a stack)cost
pushput on topstack.append(x)O(1)
popremove the topstack.pop()O(1)
peeklook at the top, don't removestack[-1]O(1)
is empty?anything there?if stack: / if not stack:O(1)
a Python list as a stack
stack = []
stack.append('a')      # push -> ['a']
stack.append('b')      # push -> ['a', 'b']     top is 'b'
if stack and stack[-1] == 'b':   # check empty BEFORE peeking
    stack.pop()        # pop  -> ['a']
Check for empty firststack[-1] or stack.pop() on an empty list raises IndexError. Here, the very first letter has nothing to compare with, so the check if stack and … is needed from the start. Python stops at stack if it's empty and never runs the peek.

The undo pattern

We read the input left to right. Each new item looks at the most recent item that is still alive (the top). If they cancel, we pop the top and drop the new item. If not, we push the new item. The stack is perfect here because, after a pop, the item underneath automatically becomes the new "most recent" one, ready to be compared with the next item. That's exactly the "after one pair vanishes, the neighbours meet" effect this problem needs.

Strings are immutable

Immutable = can't be changed in place. Deleting from the middle of a string means building a new string, which copies everything: O(n) per change. So we build the answer separately, in a list (Python) or a StringBuilder (Java), and join it into a string at the end.

The TLE rule the teacher uses

Roughly, about 10⁸ simple operations is the limit before TLE (Time Limit Exceeded). Around 4–5·10⁸ sometimes still passes if the code is simple, but once you reach 10⁹ or more, it will surely fail.


Part A · Brute force: scan again and again with a flag

LeetCode 1047

1The question in simple words

You get a string s of lowercase letters. A removal means: pick two adjacent (side by side) letters that are equal, and delete both. Keep doing removals until no two equal letters are next to each other. Return the final string. (LeetCode promises the final answer is the same whatever order you remove in.)

sabbaca"bb" vanish → "aaca"
aacanow the two a's touch → vanish → "ca"
canothing equal side by side → answer "ca"

Notice the chain reaction: the two a's were not neighbours at the start. They only meet after the b's disappear. That's the whole difficulty of the problem.

Another example: "azxxzy" → remove "xx" → "azzy" → remove "zz" → "ay".

2What the constraints tell us

3Intuition: delete pairs, then check again

The natural first idea: walk with an index i. If s[i] and s[i+1] are the same, they're a duplicate pair, so don't copy them. Otherwise copy s[i] into a new string (call it s1). Because strings can't be changed, we build the result in a separate builder.

But one scan isn't enough. In "abbaca", the first scan removes "bb" and gives "aaca". The two a's only became neighbours because of that removal. So we have to scan again. And again, until a whole scan removes nothing.

4Building the logic from the example

One scan over "abbaca"

  1. i = 0: 'a' vs next 'b' → different → copy 'a'. s1 = "a".
  2. i = 1: 'b' vs next 'b' → same → this is a pair, skip both (jump i to 3).
  3. i = 3: 'a' vs next 'c' → different → copy 'a'. s1 = "aa".
  4. i = 4: 'c' vs 'a' → copy 'c'. i = 5: 'a' is the last letter → copy. s1 = "aaca".
Doubt 1: when we find "bb", why skip exactly two letters and not every b in the run?
→ A removal takes letters two at a time. If the string were "abbba", we remove one pair "bb" and one b is left over: "aba". If you skipped every b in the run, you'd delete that leftover b too, which is wrong. So skip the pair, then carry on normally from the next letter. If another pair starts there, it'll be caught then. (The teacher talks about "one more b" here; the takeaway is: remove pairs, never single letters.)

Why the scan must repeat: the flag

After one scan we can't be sure we're done, because a removal may have created a new neighbour pair. So the teacher keeps a flag (a True/False variable, here removed):

For "abbaca": scan 1 → "aaca" (removed); scan 2 → "ca" (removed); scan 3 → "ca", nothing removed → stop. Three scans.

How slow is it? The teacher's worst case

For "abbaca" (length 6) we needed only 3 scans, so you might think it's "a bit more than O(n)". The number of scans depends on how the duplicates are arranged. We always judge by the worst case, so she builds a nasty one: "abcddcba".

scanstring beforeremovedstring after
1abcddcbaddabccba
2abccbaccabba
3abbabbaa
4aaaa""
5""nothing → flag stays Falsestop

Each scan removes just one pair from the middle. With n letters that's about n/2 scans, each costing O(n) → O(n²). For n = 10⁵ that's around 10¹⁰ steps → TLE. She submitted it and it did TLE.

5Approach steps

  1. removed = True so the first scan happens.
  2. While removed: set it to False, make an empty builder sb, and i = 0.
  3. While i < n: if i + 1 < n and s[i] == s[i+1] → skip both (i += 2) and set removed = True. Else copy s[i] and i += 1.
  4. After the scan, s = "".join(sb).
  5. When a scan removes nothing, return s.

6Code (Python)

Brute force: repeated scans, O(n²) worst case (TLE)
class Solution:
    def removeDuplicates(self, s):
        removed = True                       # run at least one scan
        while removed:
            removed = False                  # assume this scan changes nothing
            sb = []                          # the builder for this scan
            i = 0
            n = len(s)
            while i < n:
                if i + 1 < n and s[i] == s[i + 1]:
                    i += 2                   # skip the pair
                    removed = True           # something changed, scan again
                else:
                    sb.append(s[i])          # keep this letter
                    i += 1
            s = "".join(sb)                  # the new, shorter string
        return s

7Code line by line

linewhat it means
removed = TrueStart as True only so the outer loop runs the first time.
while removed: removed = FalseStart a new scan and assume nothing will change. If the scan deletes something, it switches the flag back on.
sb = []A fresh builder for this scan (Python's version of a StringBuilder).
if i + 1 < n and s[i] == s[i + 1]:Is there a next letter, and is it equal to this one? The i + 1 < n part stops us reading past the end.
i += 2 removed = TrueDrop both letters of the pair. Raise the flag: a new pair may have formed around this spot.
sb.append(s[i]) i += 1Not a pair: keep this letter, move one step.
s = "".join(sb)The result of this scan becomes the input of the next one.
return sReached only after a scan with no removals, so no adjacent equal letters remain.

8Dry run

s = "abbaca".

scanis[i], s[i+1]actionsb afterremoved
1 (s = "abbaca")0a, bkeep aaFalse
1b, bskip pair, i → 3aTrue
3a, ckeep aaaTrue
4c, akeep caacTrue
5a, (end)keep aaacaTrue
2 (s = "aaca")0a, askip pair, i → 2(empty)True
2c, akeep ccTrue
3a, (end)keep acaTrue
3 (s = "ca")0, 1c,a / a,endkeep bothcaFalse → stop

Answer "ca" ✓

9Complexity & remember

Remember the brute forceScan, drop equal neighbour pairs, raise a flag if anything was dropped, scan again until a scan drops nothing. Correct, but each scan only "sees" pairs that exist right now, so chain reactions need many scans.

Part B · Stack: compare with the top

1The question (same as Part A)

Same question: keep deleting equal neighbour pairs until none is left. New goal: one single pass.

2What the constraints tell us

n up to 10⁵ → we need O(n) (or O(n log n)). One pass with O(1) work per letter is the target.

3Intuition: "I need to look back" → stack

The teacher spots the key moment in the brute force: after "bb" disappears, the next 'a' has to match with the previous 'a', which we already passed and stored. We're moving forward, but sometimes we must go back and delete the latest kept letter. "Go back to the most recent one and delete it" = pop from a stack.

So the stack holds the letters we've kept so far, and its top is the latest one. For each new letter:

The chain reaction now happens for free. After a pop, the new top is the letter that sat before the removed pair, which is exactly the letter that now touches the next one.

Doubt 1: what does the stack "remember" at any moment?
→ The answer for the part of the string we've read so far. It never contains two equal letters next to each other, because any such pair would have been cancelled when the second one arrived. So the final stack is the final answer.

4Building the logic from the example

"abbaca": push a; push b; next b equals top b → pop; next a equals top a → pop (that's the chain reaction, in one step!); push c; push a. Stack = [c, a] → "ca". One pass, no rescanning.

Doubt 2: what about "aaa"?
→ push a; second a equals top → pop (stack empty); third a: stack is empty, nothing to compare → push. Answer "a" ✓. Removals take pairs, so one 'a' must stay. The empty check is what stops a crash at the third 'a'.

5Approach steps

  1. Make an empty stack.
  2. For each letter ch: if the stack isn't empty and its top equals ch → pop. Otherwise → push ch.
  3. Join the stack (bottom to top) into a string and return it.

6Code (Python)

Stack, O(n) time, O(n) space (the teacher's Python version)
class Solution:
    def removeDuplicates(self, s):
        stack = []
        for ch in s:
            if stack and stack[-1] == ch:   # same as the latest kept letter?
                stack.pop()                 # both vanish
            else:
                stack.append(ch)            # keep it (for now)
        return "".join(stack)

7Code line by line

linewhat it means
stack = []Letters kept so far. The top is the latest kept letter.
for ch in s:One pass, left to right.
if stack and stack[-1] == ch:Only peek if the stack has something. Then: is the new letter the same as the latest kept one?
stack.pop()Remove the old one. We don't push the new one, so the pair is gone.
stack.append(ch)No match: this letter survives, for now. A later letter may still cancel it.
return "".join(stack)Bottom to top = left to right, so join in list order.

8Dry run (stack after every step)

s = "abbaca". Stack drawn left = bottom, right = top.

index012345
sabbacagrey = cancelled, yellow = survives
icurrentwhat we pop (and why)what we pushstack AFTERanswer so far
0anothing (stack empty)a[a]"a"
1bnothing (top a ≠ b)b[a, b]"ab"
2bb: top b = bnothing[a]"a"
3aa: top a = a (the chain reaction)nothing[ ]""
4cnothing (stack empty)c[c]"c"
5anothing (top c ≠ a)a[c, a]"ca"
after i = 1
ab
after i = 3 (both pairs gone)
(empty)
after i = 5 (final)
ca

Answer "ca" ✓. On "abcddcba" (the brute force's worst case) the stack grows to [a, b, c, d], then each next letter pops one: d, c, b, a → empty. Eight steps instead of five full scans.

9Complexity & remember

Remember the stack versionFor each letter: top equals me → pop, else push. Check for empty first. The stack is the answer.

Part C · Same idea, builder instead of a stack (the interview point)

1What changes

Look at the end of Part B: we fill a stack, then convert it to a string. The teacher asks: why not keep the answer in a string builder from the start, and treat its last character as the top? Appending at the end = push. Deleting the last character = pop. Both are O(1). The logic is still the stack logic; we just don't use a Stack object.

2Why the builder is faster (in Java)

Same Big-O, but it ran faster on LeetCode. Her reason: Java's Stack class is synchronized. Every push and pop takes a lock, a safety step for multi-threaded programs, and that costs time. A StringBuilder is a plain growable character array with no locks. Also, with the builder we skip the final "stack → string" conversion step. She says explaining this difference in an interview makes a strong impression.

Doubt 1: what does this mean in Python?
→ Python has no StringBuilder, and the teacher's Python code simply uses a list as the stack (Part B). A Python list is already the fast, lock-free, array-based builder: append and pop at the end are O(1), and "".join makes the string in one go. (My note: Python's locked, thread-safe stack is queue.LifoQueue. That one is the slow "synchronized Stack" equivalent, so don't use it in DSA code.) The code below writes it the builder way, with an explicit length check and "last character", to mirror her Java.

3Code (Python, builder style)

Builder used as the stack, O(n) time, O(n) space
class Solution:
    def removeDuplicates(self, s):
        sb = []                                  # the builder
        for ch in s:
            length = len(sb)
            if length > 0 and sb[length - 1] == ch:   # last char = top
                del sb[length - 1]               # delete last char (O(1))
            else:
                sb.append(ch)                    # append = push
        return "".join(sb)                       # builder -> string
linewhat it means
length = len(sb)She computes the length first, because we can only read the last character if the builder isn't empty.
sb[length - 1] == chThe last character (index length − 1) plays the role of the stack top.
del sb[length - 1]Delete from the end only, so it's O(1). (Deleting from the middle would shift everything.)
sb.append(ch)Push.
return "".join(sb)Her Java does sb.toString().

The dry run is identical to Part B: the builder holds exactly what the stack held after every step ([a] → [a,b] → [a] → [ ] → [c] → [c,a]).

4Complexity & remember

RememberStack logic doesn't need a Stack class. Any array where you only touch the end is a stack. Java: StringBuilder beats Stack (no synchronization). Python: the list already is that array.

Part D · Revision page

A · repeated scansB · stackC · builder as stack
ideadrop neighbour pairs, rescan while the flag is ontop equals me → pop, else pushsame as B, the last char is the top
chain reaction handled byanother full scanthe new top after a popthe new last char after a delete
timeO(n²) worst ("abcddcba") → TLEO(n)O(n), faster in Java
spaceO(n)O(n)O(n)
If you remember only 5 lines 1. Removing a pair can make new neighbours meet, so you need to "look back" → stack.
2. Loop: if the stack isn't empty and the top equals ch → pop; else push ch.
3. The stack at the end is the answer (join it in order).
4. Brute force needs a flag and up to n/2 rescans → O(n²) → TLE at n = 10⁵.
5. Interview bonus: a builder/array used at its end is a stack without locks, so it's faster than Java's Stack.
Mistakes to avoid ✗ peeking at the top without checking for empty (first letter crashes)
✗ pushing the new letter after popping (the pair must vanish together)
✗ removing a whole run like "bbb" instead of one pair ("abbba" → "aba")
✗ doing only one scan in the brute force (misses chain reactions)
✗ reversing the stack before joining (bottom → top is already left → right)
test it yourself (paste under any of the solutions above)
s = Solution()
print(s.removeDuplicates("abbaca"))     # ca
print(s.removeDuplicates("azxxzy"))     # ay
print(s.removeDuplicates("abcddcba"))   # (empty string)
print(s.removeDuplicates("aaa"))        # a
print(s.removeDuplicates("abcd"))       # abcd

Based on this video: Remove All Adjacent Duplicates In String | Stack simulation & undo pattern