DSA sheet · Stack · Simulation & undo pattern

Backspace String Compare

This is the first problem of the third stack pattern: simulation and undo operations. Whenever a character in the input means "cancel what came just before", a stack is the natural tool, because the thing you cancel is always the most recent one. The teacher solves it three times: first the plain way (build the typed text in a string and delete from its end), then with a stack (same cost, but a much cleaner idea), and finally with two pointers walking from the right, which needs no extra space at all.

Why it matters: it's tagged Easy, but the teacher warns that interviewers often expect the O(1)-space version. It also teaches a lesson you'll use again: a problem can be a "stack problem" and still have a simpler, smarter solution without a stack.

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 of items where you can only touch the top. Think of a pile of plates: you put a new plate on top, and you take a plate from the top. The plate you put last is the first one you take out. This rule is called LIFO: Last In, First Out.

operationmeaningPython (a list used as a stack)cost
pushput an item on topstack.append(x)O(1)
popremove the top item (and get it back)stack.pop()O(1)
peek (top)look at the top item without removing itstack[-1]O(1)
is empty?is there anything at all?if not stack: / if stack:O(1)

In Python we don't need a special class. A normal list works as a stack, as long as we only add and remove at the end (the end of the list is the top).

a Python list as a stack
stack = []
stack.append('a')      # push  -> ['a']
stack.append('b')      # push  -> ['a', 'b']      'b' is on top
print(stack[-1])       # peek  -> 'b'
stack.pop()            # pop   -> ['a']
if stack:              # ALWAYS check before pop / peek
    stack.pop()        #       -> []
Check for empty firststack.pop() or stack[-1] on an empty list crashes with IndexError. So before every pop or peek, ask "is there anything on the stack?" This matters a lot in this problem, because a # can appear when there is nothing left to delete.

The undo pattern

In this pattern we read the input from left to right. Normal items are pushed. A special item means "undo the latest one", so we pop. Why does a stack fit so well? Because the thing we must undo is always the most recent item that is still alive, and that is exactly the top of the stack. After a pop, the item below becomes the new "most recent", so a second undo removes it. That's how several undos in a row work automatically.

Your phone's keyboard works the same way: each backspace deletes the last character still on the screen.

Strings are immutable in Python

Immutable means "can't be changed in place". When you write result = result[:-1] or result = result + ch, Python builds a brand new string and copies all the characters into it. That copy costs O(length). The teacher mentions that in Java you'd use a StringBuilder for this, and in Python you can simply use a string. Just remember each change makes a copy. A list (our stack) doesn't have that problem: append and pop at the end are O(1).


Part A · Brute force: build the typed string and delete from its end

LeetCode 844

1The question in simple words

You get two strings, s and t. Imagine typing each one into an empty text editor, one character at a time. A # is the backspace key: it deletes the character just before the cursor (the last character on the screen). The # itself never appears on the screen. Return True if both editors end up showing the same text, else False.

sab#c# deletes b → screen shows "ac"
tad#c# deletes d → screen shows "ac"

Both show "ac", so the answer is True. That's the teacher's first example.

Two more from LeetCode: s = "ab##", t = "c#d#" → both become empty "" → True. And s = "a#c", t = "b" → "c" vs "b" → False.

2What the constraints tell us

3Intuition: just do what the editor does

The most obvious idea: actually type each string into a fresh piece of text (call it s1 for s, t1 for t). A letter → add it at the end. A # → delete the last character, if there is one. At the end, compare s1 and t1.

4Building the logic from the example

Type s = "ab#c":

  1. a is a normal letter, it's safe → s1 = "a".
  2. b → s1 = "ab".
  3. # → delete the previous character from s1 → s1 = "a". The # itself is not added.
  4. c → s1 = "ac".

Type t = "ad#c" the same way: "a" → "ad" → "a" → "ac". Now s1 == t1 → True.

Is this really O(n)? The many-# case

At first it looks like one pass, so O(n). The teacher then asks: what about something like "abcdefab###", where many # come together? Every # makes us go back and delete. With an immutable string, every delete (and every add) builds a new copy of the whole text, which is O(current length). So in the worst case this approach is O(n²). Her advice here: forget the exact time for now, first get any working solution.

Doubt 1: what if # comes when s1 is empty, like "#a" or "a##"?
→ A backspace on an empty editor does nothing. So we only delete if s1 is not empty. In Python, ""[:-1] happens to give "" anyway, but write the check: it makes the meaning clear, and in the stack version (Part B) the same check prevents a crash.

5Approach steps

  1. Write a helper build(text) that returns what the editor shows.
  2. Inside: start with result = "". For each character: if it's # and result isn't empty, cut off the last character. Otherwise (a letter), add it at the end.
  3. Return build(s) == build(t).

6Code (Python)

Brute force: rebuild a string, O(n²) worst case
class Solution:
    def backspaceCompare(self, s, t):
        return self.build(s) == self.build(t)

    def build(self, text):
        result = ""
        for ch in text:
            if ch == "#":
                if len(result) > 0:          # backspace on empty does nothing
                    result = result[:-1]     # new string without the last char
            else:
                result = result + ch         # new string with ch at the end
        return result

7Code line by line

linewhat it means
return self.build(s) == self.build(t)Type both strings, then compare the two screens.
result = ""The empty editor.
if ch == "#":The backspace key.
if len(result) > 0: result = result[:-1]Delete the last character, only if there is one. [:-1] means "everything except the last character". It makes a new string (a copy).
result = result + chA letter: add it at the end. Again a new string.
return resultWhat the editor finally shows.

8Dry run

s = "ab#c", t = "ad#c".

is[i]actions1 aftert[i]actiont1 after
0aadd"a"aadd"a"
1badd"ab"dadd"ad"
2#delete last (b)"a"#delete last (d)"a"
3cadd"ac"cadd"ac"

"ac" == "ac" → True ✓

9Complexity & remember

Remember the brute forceType it out: letter → add at the end, # → delete the last one (if any). Compare the two results. Works, but it goes back every time it meets a #.

Part B · Stack: the cleaner way to undo

1The question (same as Part A)

Same question: type both strings with # as backspace, and compare the screens. Only the tool changes.

2What the constraints tell us

Same as Part A: n ≤ 200, so speed isn't the problem. The goal of this part is a cleaner idea, not a faster one. The teacher says it openly: the stack doesn't reduce the time or space, but it makes the solution simpler.

3Intuition: "go forward, then come back to delete" is a stack

The teacher points out a feeling we've had in earlier problems: we're moving forward, and suddenly something (here a #) tells us to go back and change the latest thing we saw. With one # we go back once; with many #s we go back many times. Whenever "move forward, but sometimes undo the most recent thing" appears, think of a stack. The "go back and delete" step is just a pop.

At the end, the stack holds exactly the characters left on the screen, bottom to top = left to right.

4Building the logic from the example

For s = "ab#c": push a, push b, # → pop b, push c. Stack = [a, c]. For t = "ad#c": push a, push d, # → pop d, push c. Stack = [a, c]. Same → True.

Nicer than Part A: we don't think about strings, copies or builders while scanning. We only push and pop.

Doubt 1: why "only pop if the stack is not empty"?
→ Input like "#ab" or "a##b" has a backspace with nothing to delete. On a real editor nothing happens. In code, stack.pop() on an empty list would crash. So: if stack: stack.pop(). The teacher writes exactly this check.
Doubt 2: after the loop, how do I turn the stack back into a string?
→ In Java she pops everything into a StringBuilder (which comes out reversed, but since both s and t are reversed the same way, the comparison still works). In Python it's simpler: "".join(stack) joins the list from bottom to top, which is the correct left-to-right order. You could even compare the two lists directly.

5Approach steps

  1. Helper build(text): make an empty stack.
  2. For each character: if it's #, pop if the stack is non-empty. Otherwise push the character.
  3. Turn the stack into a string and return it.
  4. Main function: return build(s) == build(t).

6Code (Python)

Stack version, O(n) time, O(n) space
class Solution:
    def backspaceCompare(self, s, t):
        return self.build(s) == self.build(t)

    def build(self, text):
        stack = []
        for ch in text:
            if ch == "#":
                if stack:              # something to delete?
                    stack.pop()        # undo the latest character
            else:
                stack.append(ch)       # a normal character stays (for now)
        return "".join(stack)          # bottom..top = left..right

7Code line by line

linewhat it means
stack = []The editor's screen, as a stack. The top is the last character on screen.
for ch in text:Read the keys in the order they were pressed.
if ch == "#": if stack: stack.pop()Backspace: remove the latest surviving character. Skip it if the screen is empty.
else: stack.append(ch)A letter: it goes on top. A later # may still remove it.
return "".join(stack)Glue what's left into a string. The teacher does this with a StringBuilder in Java.
self.build(s) == self.build(t)Same screens → True, else False.

8Dry run (stack after every step)

The teacher's longer example: s = "ab#cd##f#g", t = "ab#g". Stack drawn left = bottom, right = top.

index0123456789
sab#cd##f#g4 backspaces
icurrentwhat we pop (and why)what we pushstack AFTERscreen so far
0anothinga[a]"a"
1bnothingb[a, b]"ab"
2#b: backspace deletes the latest letternothing[a]"a"
3cnothingc[a, c]"ac"
4dnothingd[a, c, d]"acd"
5#d: latest letternothing[a, c]"ac"
6#c: now c is the latestnothing[a]"a"
7fnothingf[a, f]"af"
8#fnothing[a]"a"
9gnothingg[a, g]"ag"
after i = 4
acd
after i = 6 (two pops in a row)
a
after i = 9 (final)
ag

The top is in red. Two # in a row simply pop twice: first d, then c, which became the top after d left.

For t = "ab#g": push a, push b, pop b, push g → [a, g] → "ag". Both "ag" → True ✓

9Complexity & remember

Remember the stack versionLetter → push. # → pop if not empty. Join and compare. "Latest thing gets undone" = stack.

Part C · Optimal: two pointers from the right, O(1) space

1The question (same), the new goal

Same question. The new goal: no stack, no new strings. Only a few integer variables, so O(1) extra space, and every character looked at exactly once.

2What the constraints tell us

3Intuition: walk backwards, so you know what to skip

The teacher's key observation: a backspace only ever affects characters on its LEFT. When we walk left to right, we add a character and only later learn it must be removed, so we do the work twice (add, then undo), roughly 2n steps. If we walk right to left, we meet the # before the characters it deletes. So we already know "the next character I meet should be skipped", and we never add it in the first place.

Picture it: you read both strings from the end. You carry a small counter skip in your pocket. Each # adds 1 to it. Each letter you meet while skip > 0 is a deleted letter: throw it away and take 1 off skip. When you reach a letter with skip = 0, that letter really is on the screen. Stop there.

Do this in both strings. Now you're standing on the last surviving character of each one. If they differ, the screens differ → False right away. If they're the same, step both pointers left and repeat for the next surviving character.

Doubt 1: why not just compare s[i] and t[j] at every index?
→ Because some characters are going to be deleted anyway. In the teacher's example, at one point the pointers are on b (in one string) and d (in the other). They differ, but both are deleted by backspaces, so they never show on the screen. Comparing them would give a wrong False. So we first skip everything that's undone, and only compare characters that survive.

4Building the conditions

Condition 1: the skipping loop (one for s, one for t)

Starting from the pointer i in s, keep moving left while one of these is true:

Same for t with j and skip_t. Note: skip is a count, not a True/False, because several # can pile up. The teacher's example: three # after "c" → skip becomes 3 → the next three letters are all thrown away.

Doubt 2: why an inner while loop instead of handling one character per round of the outer loop?
→ With many # in a row, we want to get past all of them, and all the letters they delete, before comparing anything. The inner loop does that in one go. The outer loop only compares characters that are definitely on the screen.
Doubt 3: a while inside a while… isn't that O(n²)?
→ No. The teacher is clear on this. The inner loop moves the same pointer i that the outer loop uses. Whatever the inner loop skips, the outer loop never visits again; it continues from where the inner loop stopped. So every index is visited once in total. O(n), not O(n²).

Condition 2: compare the survivors

Doubt 4: where do we return True?
→ Only after the outer loop ends. Inside the loop we only ever return False. If we get through both strings and no surviving pair differed, and neither ran out early, the screens are equal.
Doubt 5: should the outer loop be while i >= 0 and j >= 0 or while i >= 0 or j >= 0?
→ It must be or: keep going while either string still has characters. With and, s = "ab", t = "b" would compare b = b, then stop because j < 0, and return True, which is wrong (the "a" is never checked). With or, the loop runs once more, the skipping loop for s stops at "a", t has run out → one valid, one not → False ✓. Also, in the video the conditions are said as "greater than zero", but the index 0 is a real character, so the checks must be >= 0.

5Approach steps

  1. i = len(s) - 1, j = len(t) - 1, skip_s = skip_t = 0.
  2. While i >= 0 or j >= 0:
  3. Move i left past every # and every letter they delete. Stop on a surviving letter (or when i < 0).
  4. Do the same for j in t.
  5. Both valid and different → False. Exactly one valid → False.
  6. Otherwise i -= 1, j -= 1.
  7. After the loop → True.

6Code (Python)

Two pointers from the right, O(n) time, O(1) space
class Solution:
    def backspaceCompare(self, s, t):
        i = len(s) - 1
        j = len(t) - 1
        skip_s = 0                    # how many # are waiting in s
        skip_t = 0                    # how many # are waiting in t

        while i >= 0 or j >= 0:
            # find the next surviving character in s
            while i >= 0:
                if s[i] == "#":
                    skip_s += 1
                    i -= 1
                elif skip_s > 0:     # this letter was backspaced
                    skip_s -= 1
                    i -= 1
                else:
                    break             # s[i] is really on the screen
            # find the next surviving character in t
            while j >= 0:
                if t[j] == "#":
                    skip_t += 1
                    j -= 1
                elif skip_t > 0:
                    skip_t -= 1
                    j -= 1
                else:
                    break

            if i >= 0 and j >= 0:
                if s[i] != t[j]:      # survivors differ
                    return False
            elif i >= 0 or j >= 0:    # only one string has a survivor
                return False

            i -= 1
            j -= 1

        return True

7Code line by line

linewhat it means
i = len(s) - 1 j = len(t) - 1Start both pointers at the last index: we read right to left.
skip_s = 0 skip_t = 0Counters of backspaces we've seen and not yet "used up". One per string.
while i >= 0 or j >= 0:Keep going while either string has characters left.
if s[i] == "#": skip_s += 1 i -= 1A backspace. It will delete one letter to its left, so remember that and move on.
elif skip_s > 0: skip_s -= 1 i -= 1A letter, but a backspace on its right deletes it. Throw it away (move left) and use up one backspace. Nothing is actually deleted; we just don't look at it. That's the "undo" with zero memory.
else: breakA letter with no pending backspace: it's on the screen. Stop here so we can compare it.
second inner whileExactly the same for t, with j and skip_t.
if i >= 0 and j >= 0: if s[i] != t[j]: return FalseBoth strings have a surviving character here. Different → the screens differ.
elif i >= 0 or j >= 0: return FalseWe get here only if not both are valid. If one still is, one screen is longer → False. (If neither is valid, both are finished, and the loop will end.)
i -= 1 j -= 1This pair matched. Move to the next character on the left.
return TrueEvery surviving pair matched and both ran out together.

8Dry run (the teacher's example)

s = "ab#cd##f#g", t = "ab#g" (both type to "ag"). The pointers start at the last index.

index0123456789
sab#cd##f#gyellow = survivors
tab#g
roundi walks over (s)skip_sj walks over (t)skip_tcompareresult so far
1i=9 'g': skip 0 → stop0j=3 'g': skip 0 → stop0g = g ✓ok, i=8, j=2
2i=8 '#' → skip 1; i=7 'f' deleted → skip 0; i=6 '#' → 1; i=5 '#' → 2; i=4 'd' deleted → 1; i=3 'c' deleted → 0; i=2 '#' → 1; i=1 'b' deleted → 0; i=0 'a': skip 0 → stop0j=2 '#' → skip 1; j=1 'b' deleted → 0; j=0 'a' → stop0a = a ✓ok, i=−1, j=−1
3loop condition: i < 0 and j < 0 → the loop endsreturn True ✓

Notice what the teacher points out in her narration: at one moment the pointers sit on f in s and b in t. A naive compare would say "different". But both have a # right after them, so both are skipped and never compared. That's the whole point of skipping first.

A False example: s = "ab", t = "b".

  1. Round 1: i stops at 'b' (index 1), j stops at 'b' (index 0). Same → i = 0, j = −1.
  2. Round 2: the loop runs because i ≥ 0. i stops at 'a'. j is already −1. One valid, one not → return False ✓

9Complexity & remember

Remember the optimal versionRead from the right. # → skip += 1. Letter with skip > 0 → skip -= 1 and move on. Letter with skip = 0 → compare it. Differ, or only one runs out → False. Loop ends → True.

The teacher's closing lesson: even when a problem is clearly from the stack family, there can be a simpler optimised idea, and spotting it comes with practice. In the beginning, thinking of the stack is already good enough.


Part D · Revision page

A · rebuild stringB · stackC · two pointers from the right
directionleft → rightleft → rightright → left
on a letteradd to the stringpushskip it if skip > 0, else compare it
on #cut the last char (if any)pop (if not empty)skip += 1
comparewhole strings at the endwhole strings at the endone survivor pair at a time, stop early on a mismatch
timeO(n²) in Python (string copies)O(n)O(n)
extra spaceO(n)O(n)O(1)
If you remember only 5 lines 1. # deletes the latest surviving character to its left = the undo pattern = a stack.
2. Stack: letter → push, # → pop only if not empty, then compare the joined stacks.
3. A backspace only affects the left side, so read from the right and you'll know what to skip.
4. Keep a count of pending # per string. Skip letters while the count is positive.
5. Compare only survivors. Mismatch or one string running out → False. True only after the loop.
Mistakes to avoid ✗ popping an empty stack on input like "#a" (crash)
✗ comparing characters that a later # deletes
✗ using a True/False flag instead of a skip counter (fails on "##")
✗ outer loop with and instead of or (misses an extra character in the longer string)
✗ index checks with > 0 instead of >= 0 (ignores index 0)
✗ thinking the nested while makes it O(n²)
test it yourself (paste under any of the solutions above)
s = Solution()
print(s.backspaceCompare("ab#c", "ad#c"))         # True
print(s.backspaceCompare("ab##", "c#d#"))         # True
print(s.backspaceCompare("a#c", "b"))             # False
print(s.backspaceCompare("ab#cd##f#g", "ab#g"))   # True
print(s.backspaceCompare("ab", "b"))              # False
print(s.backspaceCompare("#", "a#"))              # True

Based on this video: Backspace String Compare | Stack simulation & undo pattern