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 · What you must know before starting (stack, monotonic stack)
- Part A · Brute force: walk a list, delete, step back
- Part B · Optimal: a stack of survivors
- Part C · Revision page
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.
| operation | meaning | Python (a list used as a stack) |
|---|---|---|
| push | put an item on top | st.append(x) |
| pop | remove the top item (and get it back) | st.pop() |
| peek / top | look at the top item without removing it | st[-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.
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.
→ 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.
- The size of an asteroid is the number without its sign (its absolute value). −5 and 5 have the same size, 5.
- The sign is its direction: positive → moving right, negative ← moving left.
- All asteroids move at the same speed. The number is the size (mass), not the speed. The teacher stresses this because it's easy to misread.
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
- 2 ≤ n ≤ 10⁴ → at least two asteroids, so no empty-input case. And n = 10⁴ means an O(n²) solution does about 10⁸ steps, which is at the edge of TLE (Time Limit Exceeded). That's the hint that the brute force will be slow and we must find something closer to O(n).
- −1000 ≤ value ≤ 1000, and no value is 0 → tiny numbers. The teacher's rule of thumb: an int is safe up to around 10⁹; beyond that you'd need a bigger type. Here we're far below that (and in Python ints never overflow anyway).
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):
| A | B | picture | collide? | why |
|---|---|---|---|---|
| 5 | 10 | 5 → 10 → | no | both go right at the same speed, the gap never closes |
| −5 | −6 | ← 5 ← 6 | no | both go left at the same speed |
| −5 | 5 | ← 5 5 → | no | they fly away from each other |
| 5 | −5 | 5 → ← 5 | yes | they 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:
| A | B | compare sizes | result |
|---|---|---|---|
| 5 | −10 | 5 < 10 | A explodes, −10 survives |
| 5 | −2 | 5 > 2 | B explodes, 5 survives |
| 5 | −5 | 5 = 5 | both explode, nothing left |
→ 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].
- If they can't collide (not "a positive, b negative") → just move on:
i += 1. - Case 1, a > −b (B is smaller): delete B. Keep
iwhere it is, because the new neighbour at i+1 might also be a left-mover heading for A. - Case 2, a < −b (A is smaller): delete A. Now B has moved into position i and keeps flying left. It may hit the asteroid before it too. So we must step back:
i -= 1. - Case 3, a == −b: delete both. The asteroid before them and the one after them are now neighbours, so again step back.
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.
→ 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.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.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.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
- Copy the array into a list we can delete from. Set i = 0.
- While i is before the last index: let a = lst[i], b = lst[i+1].
- 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. - Otherwise → i += 1.
- Return the list.
6Code (Python)
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 lst7Code line by line
| line | what 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 -= 1 | Go 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 += 1 | No collision here, try the next pair. |
8Dry run: [5, 10, −15, 3]
| step | i | pair (a, b) | what happens | list after |
|---|---|---|---|---|
| 1 | 0 | (5, 10) | both positive, no collision → i = 1 | [5, 10, −15, 3] |
| 2 | 1 | (10, −15) | 10 < 15 → 10 explodes, step back → i = 0 | [5, −15, 3] |
| 3 | 0 | (5, −15) | 5 < 15 → 5 explodes, i is 0, can't step back | [−15, 3] |
| 4 | 0 | (−15, 3) | moving apart, no collision → i = 1 | [−15, 3] |
| 5 | 1 | 1 is not < len − 1 = 1 → loop ends | [−15, 3] ✓ |
9Complexity & remember
- Time O(n²). It looks like one loop, so you might say O(n). But i keeps going back and re-checking pairs it already passed, which makes it behave like a loop inside a loop. Also, each delete from the middle of a list shifts all later items, which is another O(n) each time. With n = 10⁴ that's up to about 10⁸ steps.
- Space O(n) for the list copy.
The teacher submits it: it is accepted but very slow. So we optimise.
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.
→ 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:
| situation | example (top, a) | pop the top? | push a? |
|---|---|---|---|
| top is smaller | (10, −15) | yes, and keep checking the next top | maybe later |
| same size | (5, −5) | yes, once | no |
| top is bigger | (10, −5) | no | no |
| no collision possible | (−2, −15), (5, 3), empty | no | yes |
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.
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.→ No. The while loop pops every smaller positive top. When it stops with a positive top, that top is ≥ −a.
5Approach steps
- Make an empty stack.
- For each asteroid a, left to right:
- While the stack isn't empty, a < 0, top > 0 and top < −a → pop (the top explodes).
- If the stack isn't empty, a < 0 and top > 0 → a explodes (don't push). If top == −a, pop the top too.
- Else → push a.
- Return the stack (bottom to top is the left-to-right order).
6Code (Python)
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→ 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
| line | what 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 st | Bottom → 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.
| i | a | what we pop (and why) | push? | stack after (bottom → top) | answer so far |
|---|---|---|---|---|---|
| 0 | −2 | nothing, stack empty | push −2 | [−2] | [−2] |
| 1 | 5 | nothing, a moves right | push 5 | [−2, 5] | [−2, 5] |
| 2 | 10 | nothing, a moves right | push 10 | [−2, 5, 10] | [−2, 5, 10] |
| 3 | −4 | nothing: top 10 is bigger than 4 | no, −4 explodes | [−2, 5, 10] | [−2, 5, 10] |
| 4 | −10 | while: 10 < 10? no. Rule 2: 10 == 10 → pop 10 | no, both explode | [−2, 5] | [−2, 5] |
| 5 | −15 | while: 5 < 15 → pop 5. Next top −2 is not > 0, stop | push −15 (top −2 moves left, no crash) | [−2, −15] | [−2, −15] |
| 6 | 3 | nothing, a moves right | push 3 | [−2, −15, 3] | [−2, −15, 3] ✓ |
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
- Time O(n). There's a while inside a for, which looks nested, but it isn't: the while loop only pops, and each asteroid is pushed at most once, so it can be popped at most once. Over the whole run there are at most n pushes and n pops: O(n) + O(n) = O(2n) = O(n). (In Java, copying the stack into the answer array is one more O(n).)
- Space O(n): the stack can hold every asteroid, e.g. when all move the same way.
Part C · Revision page
| Brute force | Stack | |
|---|---|---|
| looks back by | moving i backwards | looking at the stack top |
| left-mover wins | delete A, i -= 1 if i > 0 | while … pop() |
| right-mover wins | delete B, i stays | don't push a |
| equal | delete both, step back | pop top, don't push a |
| loop | while i < len(lst) - 1 | for a in asteroids + inner while |
| time / space | O(n²) / O(n) | O(n) / O(n) |
| left | right | result |
|---|---|---|
| + | + | never meet |
| − | − | never meet |
| − | + | fly apart, never meet |
| + | − | collide: bigger survives, equal → both gone |
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).
✗ 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 −1s = 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