DSA sheet · Stack · Monotonic stack pattern

Asteroid Collision

This is the next question in the monotonic stack pattern, right after Next Greater Element and Daily Temperatures. The teacher first lists every way two asteroids can meet, then writes a brute force that walks a list and deletes asteroids while stepping back, shows why it is O(n²), and finally replaces it with a stack that does the same job in O(n).

Why it matters: "move forward, but sometimes look back and cancel earlier items" is the signal for a stack. You will see the same shape in Remove K Digits, Backspace String Compare and the parentheses problems.

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 plates. You can only touch the top plate. The plate you put on last is the one you take off first. This rule is called LIFO: Last In, First Out.

operationmeaningPython (a list used as a stack)
pushput an item on topst.append(x)
popremove the top item (and get it back)st.pop()
peek / toplook at the top item without removing itst[-1]
is empty?are there no items?not st

A Python list works as a stack because append and pop() both act on the right end, and both are O(1). In these notes we always draw a stack left = bottom, right = top, e.g. [5, 10] means 10 is on top.

Golden safety ruleBefore st.pop() or st[-1], make sure the stack is not empty. On an empty list, both crash with an IndexError. That is why every condition below starts with st and …: Python checks st first and stops if it's empty.

What is a monotonic stack?

Monotonic means "always going one way". A monotonic stack is a stack where, before pushing a new item, we pop the items on top that the new item "beats". The stack then only remembers the items that are still useful for the future.

In Next Greater Element we popped smaller numbers because a bigger number had arrived and they would never be anyone's answer again. Here the idea is the same, but the "beating" is a collision: a big left-moving asteroid destroys the smaller right-moving asteroids at the top of the stack, one by one, until it hits something it can't beat.

Doubt: how do I know a problem wants a stack?
→ The teacher's sign: you are walking forward through the array, and a new item makes you go back to check (and maybe remove) items you already passed: the most recent one first, then the one before it. "Most recent first" is exactly LIFO, so a stack fits.

Part A · Brute force: walk a list, delete, step back

LeetCode 735

1The question in simple words

You get an array asteroids. Each number is one asteroid in a row in space. Its position in the array is its position in the row.

When two asteroids meet, the smaller one explodes. If they are the same size, both explode. Return what is left after all collisions, in order.

  asteroids = [ 5, 10, -5 ]

     5 →     10 →    ← 5          (-5 is size 5, moving left)
                10 meets -5: 10 is bigger, -5 explodes
  answer    = [ 5, 10 ]

2What the constraints tell us

3Intuition: when can two asteroids actually meet?

Before writing any code, the teacher lists all four direction pairs for two neighbours A (left one) and B (right one):

ABpicturecollide?why
5105 → 10 →noboth go right at the same speed, the gap never closes
−5−6← 5 ← 6noboth go left at the same speed
−55← 5 5 →nothey fly away from each other
5−55 → ← 5yesthey fly towards each other

A collision happens only when the left one is positive and the right one is negative. Everything else is safe.

4Building the logic from examples

The three outcomes of a collision

Once we know A > 0 and B < 0, there are three ways it can end. The teacher shows one example for each:

ABcompare sizesresult
5−105 < 10A explodes, −10 survives
5−25 > 2B explodes, 5 survives
5−55 = 5both explode, nothing left
Doubt: how do I compare sizes when B is negative?
→ Flip B's sign: -b. If b = −5, then −b = 5. So "A is bigger" is a > -b, "B is bigger" is a < -b, and "same size" is a == -b.

Walking with two neighbours, i and i+1

One asteroid alone can't tell us anything; we need a pair. So we keep an index i and look at a = lst[i] and b = lst[i+1].

Why stepping back is needed: her −15 example

  [ 5, 10, -15 ]
          i  i+1         10 vs -15 → 10 explodes
  [ 5, -15 ]             -15 is still flying left!
    i  i+1               step back: 5 vs -15 → 5 explodes
  [ -15 ]                answer

If we only moved forward, we'd stop at [5, -15], which is wrong: 5 and −15 are still flying towards each other.

Same thing for "both explode": in [5, 10, -10, -10], 10 and −10 destroy each other, leaving [5, -10]. Now 5 and the second −10 are neighbours and must be compared, so i has to point at 5 again.

Doubt: what if i is already 0 when I want to step back?
→ Then there is nothing before it, and i = −1 is not a valid index. So step back only if i > 0. For example [10, -15]: 10 explodes, the list is [-15], i stays 0, and the loop ends.
Doubt: why a while loop and not a for loop?
→ Two reasons. (1) The list shrinks as we delete, so the end point keeps changing. (2) We sometimes move i backwards. A for i in range(...) fixes both the range and the direction in advance. A while loop just re-checks its condition every time, so it doesn't care how often i goes back and forth.
Doubt: why does the loop run while i < len(lst) - 1?
→ Because we read lst[i+1]. The last valid i is the second-last index; otherwise i+1 would go past the end.
Pencil fix (Python trap): the teacher's Java turns the array into a list and removes items from it. In Python, do not write lst.remove(b): remove deletes the first item with that value, which might be an earlier asteroid. For [-5, 10, -5], lst.remove(-5) deletes the −5 at index 0 and returns the wrong answer [10, -5]. Delete by position instead: lst.pop(i+1) or lst.pop(i). The tests check this case.

5Approach steps

  1. Copy the array into a list we can delete from. Set i = 0.
  2. While i is before the last index: let a = lst[i], b = lst[i+1].
  3. If a > 0 and b < 0 (they collide):
    • a bigger → delete b (i stays).
    • b bigger → delete a, then step back if i > 0.
    • same size → delete both, then step back if i > 0.
  4. Otherwise → i += 1.
  5. Return the list.

6Code (Python)

Brute force: delete and step back
class Solution:
    def asteroidCollision(self, asteroids):
        lst = list(asteroids)            # a list we can delete from
        i = 0
        while i < len(lst) - 1:         # we read i and i+1
            a, b = lst[i], lst[i + 1]
            if a > 0 and b < 0:         # only this pair can collide
                if a > -b:               # right-mover is bigger
                    lst.pop(i + 1)       # b explodes, i stays
                elif a < -b:             # left-mover is bigger
                    lst.pop(i)           # a explodes
                    if i > 0:
                        i -= 1           # b may hit the one before
                else:                    # same size
                    lst.pop(i + 1)       # both explode
                    lst.pop(i)
                    if i > 0:
                        i -= 1           # new neighbours meet
            else:
                i += 1                   # no collision, move on
        return lst

7Code line by line

linewhat it means
lst = list(asteroids)Work on a copy we are allowed to shrink. (The teacher's Java needed this because a plain int array can't delete items.)
while i < len(lst) - 1:len(lst) is re-read every time, so the loop adapts as the list shrinks.
if a > 0 and b < 0:The only collision case: right-mover on the left, left-mover on the right.
if a > -b: lst.pop(i + 1)The left-mover is smaller and explodes. i stays, so a meets its next neighbour.
elif a < -b: lst.pop(i)The right-mover explodes. b slides into index i.
if i > 0: i -= 1Go back one so b is compared with the asteroid before it. Guarded so i never becomes −1.
lst.pop(i + 1); lst.pop(i)Same size: delete both. Pop i+1 first so index i still points at a.
else: i += 1No collision here, try the next pair.

8Dry run: [5, 10, −15, 3]

stepipair (a, b)what happenslist after
10(5, 10)both positive, no collision → i = 1[5, 10, −15, 3]
21(10, −15)10 < 15 → 10 explodes, step back → i = 0[5, −15, 3]
30(5, −15)5 < 15 → 5 explodes, i is 0, can't step back[−15, 3]
40(−15, 3)moving apart, no collision → i = 1[−15, 3]
511 is not < len − 1 = 1 → loop ends[−15, 3] ✓
step 2510-15310 explodes
step 35-153stepped back, 5 explodes
end-153

9Complexity & remember

The teacher submits it: it is accepted but very slow. So we optimise.

Remember the brute forceCollide only when left > 0, right < 0. Three outcomes: bigger survives, equal → both go. After a left-mover wins, step back (if i > 0). Use a while loop.

Part B · Optimal: a stack of survivors

1The question (same as Part A)

Same input, same output. We only change how we look back.

2What the constraints tell us

n up to 10⁴ and the brute force was slow → aim for O(n): every asteroid should be handled a constant number of times.

3Intuition: why a stack?

In the brute force, every time a left-mover won, we walked backwards to the asteroid just before it, then the one before that… always the most recent survivor first. The teacher connects this to Next Greater / Next Smaller Element: whenever you move ahead and then have to come back to check earlier values, use a stack.

So we keep a stack of survivors so far (left = bottom, right = top). When a new asteroid arrives, it can only crash into the top of the stack, because that's its nearest surviving neighbour. At the end, the stack is the answer.

What does the stack remember? All asteroids that are still alive, in order. Any left-movers in it are already safe forever (nothing on their left is coming at them). Any right-movers on top are "waiting": a future left-mover might still hit them.

Doubt: can we scan from right to left instead?
→ Yes. The teacher codes left → right and leaves right → left as practice. Going right to left, the stack holds survivors from the right side, and a new positive asteroid crashes into negative tops. The rules are the mirror image. (A version is in the test file, if you want to check yours.)

4Building the logic: pop or don't push?

Take the new asteroid a. A collision is possible only if the stack is not empty, a < 0 (moving left) and top > 0 (moving right). Then:

situationexample (top, a)pop the top?push a?
top is smaller(10, −15)yes, and keep checking the next topmaybe later
same size(5, −5)yes, onceno
top is bigger(10, −5)nono
no collision possible(−2, −15), (5, 3), emptynoyes

The key thinking: "pop" means remove a survivor that's already in the stack; "don't push" means the new asteroid itself explodes. Whatever stays in the stack at the end is the answer.

Rule 1: a while loop to pop all smaller right-movers

while st and a < 0 and st[-1] > 0 and st[-1] < -a: st.pop()

Why while and not if? Her example: stack [5, 10], new asteroid −15. It destroys 10, and then it's still flying and also destroys 5. One if would stop after 10.

Rule 2: after the loop, does the new asteroid survive?

If the loop stopped and there is still a collision (stack not empty, a < 0, top > 0), then the top is not smaller than a (otherwise the loop would have popped it). So only two possibilities are left: the top is equal or bigger. In both, a does not survive, so we don't push it. And if they're equal, the top also explodes → pop it.

If there is no collision at all → push a. That's the else.

Doubt: why must Rule 2 check st[-1] > 0 again?
→ The teacher forgot it at first and caught it on screen. Without it, a negative a with a negative top (like stack [-2], a = −15) would enter the "collision" branch, and −15 would never be pushed, even though two left-movers never collide. The answer would lose −15. So the check is compulsory.
Doubt: in Rule 2 can the top be smaller than −a?
→ No. The while loop pops every smaller positive top. When it stops with a positive top, that top is ≥ −a.

5Approach steps

  1. Make an empty stack.
  2. For each asteroid a, left to right:
  3. While the stack isn't empty, a < 0, top > 0 and top < −a → pop (the top explodes).
  4. If the stack isn't empty, a < 0 and top > 0 → a explodes (don't push). If top == −a, pop the top too.
  5. Else → push a.
  6. Return the stack (bottom to top is the left-to-right order).

6Code (Python)

Optimal: stack of survivors
class Solution:
    def asteroidCollision(self, asteroids):
        st = []                                   # survivors so far
        for a in asteroids:
            # a (moving left) destroys every smaller right-mover on top
            while st and a < 0 and st[-1] > 0 and st[-1] < -a:
                st.pop()
            if st and a < 0 and st[-1] > 0:     # still a collision
                if st[-1] == -a:                  # same size: both go
                    st.pop()
                # top is equal or bigger: a explodes, don't push
            else:
                st.append(a)                      # a survives (for now)
        return st
Doubt: the teacher's Java builds the answer array from the back. Why don't we?
→ In Java she pops the stack to fill an int array. Popping gives the top first, which is the last asteroid, so she fills the array from index size−1 down to 0, which avoids a separate reverse step (still O(n)). In Python the list already holds the survivors bottom-to-top in the right order, so we just return it. That's why she says the Python version is shorter.

7Code line by line

linewhat it means
st = []The stack of survivors. At the end it is the answer.
for a in asteroids:Handle each asteroid once, left to right.
while st and a < 0 and st[-1] > 0 and st[-1] < -a:st first (safety). Then: a moves left, top moves right, and the top is smaller → the top explodes.
st.pop()Remove it, then check the new top.
if st and a < 0 and st[-1] > 0:A collision is still happening, and the top is ≥ a's size, so a will not survive.
if st[-1] == -a: st.pop()Equal sizes: the top goes too.
else: st.append(a)No collision with the top (empty stack, a moving right, or top moving left) → a survives for now.
return stBottom → top = left → right order.

8Dry run: [−2, 5, 10, −4, −10, −15, 3]

This one example hits every case: a left-mover at the start, "top bigger", "equal", "pop many", and two left-movers side by side.

iawhat we pop (and why)push?stack after (bottom → top)answer so far
0−2nothing, stack emptypush −2[−2][−2]
15nothing, a moves rightpush 5[−2, 5][−2, 5]
210nothing, a moves rightpush 10[−2, 5, 10][−2, 5, 10]
3−4nothing: top 10 is bigger than 4no, −4 explodes[−2, 5, 10][−2, 5, 10]
4−10while: 10 < 10? no. Rule 2: 10 == 10 → pop 10no, both explode[−2, 5][−2, 5]
5−15while: 5 < 15 → pop 5. Next top −2 is not > 0, stoppush −15 (top −2 moves left, no crash)[−2, −15][−2, −15]
63nothing, a moves rightpush 3[−2, −15, 3][−2, −15, 3] ✓
after i = 3 (−4 bounced off)
-2510
i = 5, −15 has just popped 5
-2
final
-2-153

The top is drawn in red. At i = 5, without the st[-1] > 0 check in Rule 2, −15 would wrongly be thrown away because the top (−2) is negative.

The teacher's own example, [5, 10, -5]: push 5, push 10, then −5 meets top 10, which is bigger → −5 isn't pushed → answer [5, 10].

9Complexity & remember

Remember the stack versionStack = survivors. New left-mover: pop smaller right-movers with a while; then if a right-mover is still on top, the new one dies (pop the top too if equal). Otherwise push. Return the stack.

Part C · Revision page

Brute forceStack
looks back bymoving i backwardslooking at the stack top
left-mover winsdelete A, i -= 1 if i > 0while … pop()
right-mover winsdelete B, i staysdon't push a
equaldelete both, step backpop top, don't push a
loopwhile i < len(lst) - 1for a in asteroids + inner while
time / spaceO(n²) / O(n)O(n) / O(n)
leftrightresult
++never meet
−−never meet
−+fly apart, never meet
+−collide: bigger survives, equal → both gone
If you remember only 5 lines 1. Sign = direction, size = absolute value, same speed for all.
2. Only "positive then negative" collide.
3. Stack holds survivors; the new asteroid can only hit the top.
4. while pop smaller positive tops; then if a positive top remains, the new one dies (equal → pop too); else push.
5. Every item is pushed once and popped at most once → O(n).
Mistakes to avoid ✗ treating the number as speed
✗ thinking (−, +) collide (they fly apart)
✗ using if instead of while for the pops
✗ forgetting st[-1] > 0 in the second check
✗ peeking at an empty stack
✗ in the brute force: lst.remove(value) instead of deleting by index, or letting i go to −1
test it yourself (paste under either solution)
s = Solution()
print(s.asteroidCollision([5, 10, -5]))                  # [5, 10]
print(s.asteroidCollision([8, -8]))                      # []
print(s.asteroidCollision([10, 2, -5]))                  # [10]
print(s.asteroidCollision([5, 10, -15, 3]))              # [-15, 3]
print(s.asteroidCollision([-2, 5, 10, -4, -10, -15, 3])) # [-2, -15, 3]
print(s.asteroidCollision([-5, 10, -5]))                 # [-5, 10]

Based on this video: Asteroid Collision | Stack