DSA sheet · Stack · Simulation & undo pattern

Make The String Great

The third problem of the simulation and undo pattern, and the teacher says it's almost a copy of Remove All Adjacent Duplicates. The only new thing is which neighbours cancel: the same letter in opposite cases, like "e" next to "E". She shows a brute force that deletes in place and steps back (O(n²)), then the stack (O(n)), and then again the faster string builder version. The real lesson is writing the "do these two cancel?" check correctly.

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 plates. The last item put in is the first taken out: 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 topstack[-1]O(1)
is empty?anything there?if stack:O(1)
Check for empty firstNever peek or pop an empty list (IndexError). Write the check first: if stack and stack[-1] …. Python stops at the first False, so the peek never runs on an empty stack.

The undo pattern

Read left to right. A new item compares itself with the latest surviving item (the top). If the two cancel, pop and don't push the new one. Otherwise, push. After a pop, the item below becomes the new top, so the chain reaction "the neighbours of a removed pair now touch" is handled automatically.

Upper case, lower case, and comparing them

"e" is lowercase (small), "E" is uppercase (capital). They're the same letter but different characters: 'e' == 'E' is False.

Strings are immutable

Deleting from a string means making a new one (an O(n) copy). Even in a list, deleting from the middle is O(n), because every item to the right shifts one place left. Deleting from the end is O(1). That difference decides the complexity here.


Part A · Brute force: delete in place and step back

LeetCode 1544

1The question in simple words

You get a string s with lowercase and uppercase letters. Two neighbouring characters are a bad pair if they're the same letter but in opposite cases: "eE" or "Ee". Delete bad pairs, again and again, until none is left. The string is then "good" (great). Return it. An empty string also counts as good.

ex 1leEeetcode"eE" (index 1–2) is bad → "leetcode"
ex 2abBAcC"bB" → "aAcC" → "aA" → "cC" → ""
ex 3snothing to delete → "s"

In example 1, once "eE" is deleted we get "leetcode", which has "ee" in it. Both are lowercase, so they're not a bad pair and they stay. Both directions count as bad, though: "eE" and "Ee".

2What the constraints tell us

3Intuition: walk, delete, step back

Stand at index i and look at i and i+1. If they're a bad pair, delete both from the string itself. Everything on the right slides two places left. Then step back one place (i -= 1), because the letter before the deleted pair now touches the letter after it, and they might form a new bad pair. If they're not a bad pair, move forward.

4Building the logic from the examples

Example 1, "leEeetcode"

i = 0: 'l','e' → different letters, move on. i = 1: 'e','E' → same letter, opposite case → delete both → "leetcode". Step back to i = 0: 'l','e' → no. Then 'e','e' → same case, not bad → move on… nothing else is deleted → "leetcode".

Example 2, "abBAcC": why we must step back

  1. i = 0: 'a','b' → different letters → i = 1.
  2. i = 1: 'b','B' → bad → delete → "aAcC". If we just stayed going forward from i = 1 we'd look at 'A','c' and miss the new pair "aA". So step back: i = 0.
  3. i = 0: 'a','A' → bad → delete → "cC". We're already at index 0, so we can't step back; i stays 0.
  4. i = 0: 'c','C' → bad → delete → "". Nothing left → answer "".
Doubt 1: why step back only one place, and why not below 0?
→ A deletion only creates one new neighbour pair: (the letter just before the hole, the letter just after it). The letter before the hole is now at index i − 1, so i − 1 is the only place a new bad pair can start. At i = 0 there's nothing before, so we stay at 0. The teacher refers back to the previous problem for this reasoning.

How do we test "bad pair" in code?

The teacher builds the check in two parts:

The bad-pair testa.lower() == b.lower() and a != b
"Same letter after lowering, but not the same character as written" = one small and one capital of the same letter.
Doubt 2: is there a shorter test?
→ (My note, not from the video.) In ASCII, a capital and its small letter are exactly 32 apart ('A' = 65, 'a' = 97). So abs(ord(a) - ord(b)) == 32 is the same test. The teacher's two-part version is easier to read and explain, so prefer it in interviews.

Cost of this approach

Moving i over the string is O(n). But each deletion in the middle shifts all the letters on its right, another O(n). So the total is O(n²). Fine for n = 100, but not something you'd want for big inputs.

5Approach steps

  1. Copy the string into a list arr (so we can delete from it).
  2. i = 0. While i < len(arr) - 1 (there is a pair to look at):
  3. If arr[i], arr[i+1] is a bad pair → delete both. If i > 0, do i -= 1.
  4. Else → i += 1.
  5. Return the joined list.

6Code (Python)

Brute force: delete in place, step back, O(n²)
class Solution:
    def makeGood(self, s):
        arr = list(s)                      # a string can't be changed, a list can
        i = 0
        while i < len(arr) - 1:            # is there a pair (i, i+1)?
            a, b = arr[i], arr[i + 1]
            if a.lower() == b.lower() and a != b:
                del arr[i:i + 2]           # O(n): the right part shifts left
                if i > 0:
                    i -= 1                 # the new neighbours may now match
            else:
                i += 1
        return "".join(arr)

7Code line by line

linewhat it means
arr = list(s)Python strings are immutable, so work on a list of characters.
while i < len(arr) - 1:Recalculated every time, because the list shrinks after each deletion.
a.lower() == b.lower() and a != bSame letter, opposite case.
del arr[i:i + 2]Remove both characters. Everything to the right moves 2 places left (this is the hidden O(n)).
if i > 0: i -= 1Step back so the letter before the hole is compared with the letter after it. Not below 0.
i += 1Not a bad pair, move on.

8Dry run

s = "abBAcC".

stepipair looked atbad?arr afternext i
10a, bno (different letters)a b B A c C1
21b, Byesa A c C0 (step back)
30a, Ayesc C0 (can't go below 0)
40c, Cyes(empty)loop ends

Answer "" ✓

9Complexity & remember

Remember the brute forceBad pair → delete both and step back one. Not bad → step forward. Works, but every middle deletion costs O(n).

Part B · Stack: compare with the top

1The question (same as Part A)

Same question. New goal: one pass, and only cheap O(1) deletions from the end.

2What the constraints tell us

n ≤ 100, so speed isn't forced on us. But whenever we have to look back at the previous surviving character, the teacher's rule is: use a stack, it gives O(n).

3Intuition: the stack holds the "good so far" part

Keep the characters that survive so far on a stack. The top is the last surviving character. For each new character ch:

Stepping back in Part A is the same as looking at the new top after a pop. But a pop costs O(1) instead of shifting the whole string.

4Building the logic from the example

"leEeetcode": push l; push e; 'E' vs top 'e' → bad → pop e, skip E; push e; next 'e' vs top 'e' → same case, not bad → push e; then t, c, o, d, e pushed. Stack = l e e t c o d e.

Turning the stack into the answer (her array trick)

The answer must be a string. If you pop items one by one, they come out top first, which is the end of the answer. Adding each popped item to the front, or adding at the back and then reversing, is extra work. The teacher's trick: make an array of size len(stack), and fill it from the last index backwards as you pop. The first item popped (the last character) goes into the last slot, and so on. Then turn the array into a string.

Doubt 1: in Python, do I really need the pop-into-array step?
→ No. A Python list keeps its order, so "".join(stack) already gives bottom → top = left → right. The code below shows her array trick so you understand it (it's how you'd do it with Java's Stack). Part C shows the simpler way. The teacher herself calls the stack + extra array version "a little messy", which leads into Part C.

5Approach steps

  1. Empty stack. For each ch in s:
  2. If the stack isn't empty and (top, ch) is a bad pair → pop. Else → push ch.
  3. Make result of size len(stack). Pop everything, filling result from the last index to index 0.
  4. Return the joined result.

6Code (Python)

Stack + fill the answer from the back, O(n) time, O(n) space
class Solution:
    def makeGood(self, s):
        stack = []
        for ch in s:
            if stack and stack[-1].lower() == ch.lower() and stack[-1] != ch:
                stack.pop()                    # bad pair: both vanish
            else:
                stack.append(ch)

        result = [""] * len(stack)             # one slot per surviving char
        k = len(stack) - 1                     # fill from the back
        while stack:
            result[k] = stack.pop()            # top = last character
            k -= 1
        return "".join(result)

7Code line by line

linewhat it means
if stack and …Only compare when there is a top to compare with.
stack[-1].lower() == ch.lower()Same letter, case ignored.
and stack[-1] != ch…but not the identical character, so the cases must differ.
stack.pop()Remove the top. We don't push ch, so the pair is gone.
stack.append(ch)No cancellation: ch survives for now.
result = [""] * len(stack)An array exactly as long as the answer.
result[k] = stack.pop() k -= 1The top is the last character, so it goes in the last slot; then move one slot left. No reversing needed.
return "".join(result)Array → string.

8Dry run (stack after every step)

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

icurrentwhat we pop (and why)what we pushstack AFTERanswer so far
0anothing (stack empty)a[a]"a"
1bnothing ('a' vs 'b': different letters)b[a, b]"ab"
2Bb: 'b' and 'B' are the same letter, opposite casenothing[a]"a"
3Aa: 'a' and 'A' (the chain reaction)nothing[ ]""
4cnothing (stack empty)c[c]"c"
5Cc: 'c' and 'C'nothing[ ]""
after i = 1
ab
after i = 3
(empty)
after i = 5 (final)
(empty)

Answer "" ✓. And for "leEeetcode" the final stack is [l, e, e, t, c, o, d, e]. Popping fills the result from the back: 'e' → slot 7, 'd' → slot 6, 'o' → slot 5, … → "leetcode" ✓.

slot01234567
resultleetcodeyellow = the first three pops

9Complexity & remember

Remember the stack versionTop and ch are the same letter in opposite case → pop, else push. To turn a stack into a string without reversing, fill an array from the back while popping.

Part C · Same idea, builder instead of a stack

1What changes

The teacher asks whether we can drop the extra stack and the extra result array. Yes: keep the answer in a string builder from the start and use its last character as the top. Deleting the last character is O(1), just like a pop. At the end, the builder already is the answer in the right order, so we convert it once and we're done. Stack logic, no Stack object.

2Why it's faster

The space is still linear, so in Big-O nothing changes. But it runs faster, for the same reason as in the previous problem: Java's Stack is synchronized (it takes a lock on every push and pop), while a StringBuilder is a plain array with no locks. And there's no copy into a second array at the end.

Doubt 1: and in Python?
→ A Python list is that plain, lock-free array. Using it as the builder (append at the end, delete the last item, join at the end) is the best Python version. It's also what the teacher's short Python solution does.
Doubt 2: she hit errors while typing the Java code on screen. What were they?
→ Only typing slips: a missing bracket in the long if-condition, and looping over the string without .toCharArray(). Nothing changed in the logic. The lesson: with a long condition like this, it's easy to misplace a bracket, so build it piece by piece (same letter? opposite case?).

3Code (Python, builder style)

Builder used as the stack, O(n) time, O(n) space
class Solution:
    def makeGood(self, s):
        sb = []
        for ch in s:
            length = len(sb)
            if (length > 0
                    and sb[length - 1].lower() == ch.lower()   # same letter
                    and sb[length - 1] != ch):                 # opposite case
                del sb[length - 1]          # delete the last char, O(1)
            else:
                sb.append(ch)
        return "".join(sb)
linewhat it means
length = len(sb)Needed to check it's not empty, and to find the last index.
sb[length - 1]The last character = the top.
del sb[length - 1]Pop, but on the builder.
return "".join(sb)Already in the right order. No back-filling needed.

Dry run: identical to Part B. After each step the builder holds exactly what the stack held: [a] → [a,b] → [a] → [ ] → [c] → [ ].

4Complexity & remember

RememberIf you can identify why it's a stack problem and know when a builder beats the Stack class, you're understanding DSA in depth, not just memorising.

Part D · Revision page

A · delete in placeB · stack + arrayC · builder as stack
on a bad pairdelete both, step back 1pop the top, skip chdelete the last char, skip ch
deletion costO(n) (shift)O(1)O(1)
build the answerjoin what's leftpop into an array, filling from the backjoin the builder
timeO(n²)O(n)O(n), fastest
spaceO(n)O(n) + O(n)O(n)
pairlower() equal?raw different?bad pair?
e, Eyesyesyes → delete
E, eyesyesyes → delete
e, eyesnono (same case)
e, Anoyesno (different letters)
If you remember only 5 lines 1. Same as Remove Adjacent Duplicates; only the cancel rule changes.
2. Cancel rule: a.lower() == b.lower() and a != b.
3. Loop: stack not empty and the top cancels ch → pop; else push.
4. Brute force deletes in the middle (O(n) shift) and steps back one → O(n²).
5. Builder/list used at its end = a stack without locks, and no reverse step at the end.
Mistakes to avoid ✗ checking only lower() == (deletes "ee")
✗ checking only != (deletes "eA")
✗ forgetting the empty check before reading the top
✗ in the brute force, not stepping back after a delete (misses "aA" in "abBA")
✗ popping into a string by adding at the end (comes out reversed)
test it yourself (paste under any of the solutions above)
s = Solution()
print(repr(s.makeGood("leEeetcode")))   # 'leetcode'
print(repr(s.makeGood("abBAcC")))       # ''
print(repr(s.makeGood("s")))            # 's'
print(repr(s.makeGood("aa")))           # 'aa'  (same case stays)
print(repr(s.makeGood("abcCBA")))       # ''

Based on this video: Make The String Great | Stack simulation & undo pattern