DSA sheet · Linked List · Stack / hash map pattern

Remove Nodes From Linked List

This is the second problem in the linked list + stack pattern, and the teacher solves it four ways, each one fixing a weakness of the one before: (A) copy values into an array and check every pair, (B) the same check done directly on the nodes, (C) a monotonic stack, the "next greater element" trick, which brings the time down to O(n), and (D) reverse + running maximum, which is O(n) time with no extra space. It's a great problem for seeing why a stack turns O(n²) into O(n), and how reversing a list lets you "walk backwards".

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 linked list node?

A linked list is a chain of nodes. Each node has a value (val) and a link to the next node (next). The first node is the head. The last node's next is None.

given by LeetCode, don't write this in the solution
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
head
 ↓
[5] → [2] → [13] → [3] → [8] → None

Walking, and why there's no index

We walk with a pointer: curr = head, then curr = curr.next until curr is None. There is no list[3]. Reaching position i means walking i steps (O(n)). And we can only walk forward: a node knows its next, not its previous one. Keep that in mind, because this problem is about what lies to the right of each node.

Deleting a node needs the node before it

To remove node X, the node before X must skip over it: prev.next = X.next. Nothing points to X any more, so it's gone from the list. That's why deleting usually needs a prev pointer.

before:  [prev] → [X] → [Y]
after:   [prev] ─────→ [Y]         prev.next = X.next

Save next before changing a pointer

next is the only road to the rest of the list. If you overwrite a.next and haven't kept the old value somewhere, everything after it is lost. Reversal (Part D) is the main place this matters: we save nxt = curr.next before turning the arrow around.

The dummy node

If the head itself must be deleted, there's no node before it, so prev.next = … can't work. Instead of a special case ("move head forward"), we put a fake node in front: dummy = ListNode(0, head). Now every real node has a node before it. The dummy never moves, and at the end the real head is dummy.next, even if the old head was deleted.

[0] → [5] → [2] → [13] → [3] → [8] → None
 ↑
dummy = prev   (start)

A stack

A stack is a pile: you add on top (push) and remove from the top (pop). Last in, first out. In Python a plain list works: st.append(x) pushes, st.pop() pops, st[-1] peeks at the top.

Reversing a list (prev / curr / nxt)

Part D needs the standard reversal from earlier in the sheet. Walk once, and turn each arrow to point backwards. Save nxt first, otherwise the rest of the list is lost.

reverse a linked list (from the earlier video)
def reverse(head):
    prev = None
    curr = head
    while curr:
        nxt = curr.next      # 1. save the road ahead
        curr.next = prev     # 2. turn the arrow back
        prev = curr          # 3. step prev forward
        curr = nxt           # 4. step curr forward
    return prev              # the old last node is the new head

Part A · Brute force 1: copy values, check every pair

LeetCode 2487 · Remove Nodes From Linked List

1The question in simple words

You get the head of a linked list. Remove every node that has a strictly greater value somewhere to its right. Return the head of what's left.

input:   [5] → [2] → [13] → [3] → [8] → None

5  → there is a bigger value to its right (13)   → remove
2  → 13 is to its right                         → remove
13 → nothing bigger after it (3, 8)              → keep
3  → 8 is to its right                          → remove
8  → it's the last node, nothing after it       → keep

output:  [13] → [8] → None

The teacher points out that for 5 we don't care which bigger value we find (13 or 8). One is enough to delete it. Also, the last node is always kept, because nothing is to its right. And what's left is always non-increasing (13 ≥ 8): if a kept node had a bigger kept node after it, it should have been removed.

2What the constraints tell us

3Intuition

Pretend it's an array: [5, 2, 13, 3, 8]. For each position i, look at everything to its right (j = i+1 … end). If any value is bigger, i is thrown out. If none is, i survives.

4Building the logic

Doubt: why the flag? Can't I just add the value at the end of the inner loop?
→ The inner loop can end in two ways: we found a bigger value and broke out, or j simply reached the end. Only the second one means "keep". The flag remembers which way the loop ended.

5Approach steps

  1. Copy all values into arr.
  2. For each i: scan j = i+1 … n−1. If arr[j] > arr[i], mark found and stop.
  3. If not found, append arr[i] to ans.
  4. Build a list from ans and return it.

6Code (Python)

Brute force 1: array + two loops (TLE)
class Solution:
    def removeNodes(self, head):
        arr = []
        curr = head
        while curr:                       # copy the values
            arr.append(curr.val)
            curr = curr.next

        ans = []
        n = len(arr)
        for i in range(n):
            found = False                 # is there a bigger value to the right?
            for j in range(i + 1, n):
                if arr[j] > arr[i]:
                    found = True
                    break
            if not found:
                ans.append(arr[i])        # nobody bigger: keep it

        dummy = ListNode(0)
        tail = dummy
        for v in ans:                     # build the answer list
            tail.next = ListNode(v)
            tail = tail.next
        return dummy.next

7Code line by line

linewhat it means
arr.append(curr.val)Read the list into an array, so we can use indices i and j.
found = FalseReset the flag for each new i.
for j in range(i + 1, n):Only look to the right of i.
if arr[j] > arr[i]: found = True; breakOne bigger value is enough. Stop scanning.
if not found: ans.append(arr[i])The scan reached the end without finding anything bigger, so i survives.
tail.next = ListNode(v)Build a fresh list from the survivors.

8Dry run

i (value)j scansfound?ans after
0 (5)2 ✗, 13 ✓ stopyes → drop[]
1 (2)13 ✓ stopyes → drop[]
2 (13)3 ✗, 8 ✗, endno → keep[13]
3 (3)8 ✓ stopyes → drop[13]
4 (8)(nothing to scan)no → keep[13, 8]

Build → 13 → 8 → None ✓

9Complexity & remember

RememberCorrect but too slow (n² with n = 10⁵), and it copies values out of the list, which linked list interviews don't like.

Part B · Brute force 2: work on the nodes, delete with prev

1The question

Same question. New rule: don't store the values anywhere, play with the nodes directly.

2What the constraints tell us

Same as Part A. This version is still O(n²), so it will also TLE. The goal here is to practise deleting nodes in place.

3Intuition

The same "i and j" idea, but with pointers: curr plays the role of i, and a second pointer temp runs from curr.next to the end looking for something bigger. If it finds one, curr must be removed from the list.

4Building the logic

Deleting needs the previous node → dummy + prev

To delete curr we need prev.next = curr.next. But the head (5) has no previous node. The teacher says we could special-case it by moving the head forward, but why make it complicated? Put a dummy before the head and start prev at the dummy. The dummy never moves, so dummy.next is always the current head.

The scan

For each curr: temp = curr.next, delete = False. While temp exists: if temp.val > curr.val, set the flag and break. Otherwise move temp forward (not curr! we're still looking for something bigger than curr).

After the scan: two cases

Either way, curr = curr.next afterwards.

Doubt: the while loop can stop because of break or because temp became None. How do I know which?
→ That's the job of the delete flag. Only if it is True did we really find a bigger node. If temp just ran off the end, the flag is still False and curr stays.
Doubt: after prev.next = curr.next, curr (5) still points to 2. Is that a problem?
→ No. Nothing in the list points to 5 any more, so it's unreachable from the head. Its own outgoing link doesn't matter. We even use it: curr = curr.next still takes us to 2.
Doubt: loop until curr is None, or until curr.next is None?
→ Both work. The last node never gets deleted (nothing to its right), so the teacher notes we could stop at the second-to-last node. Running until curr is None is simpler and also handles the last node correctly (its scan finds nothing → keep).

5Approach steps

  1. dummy = ListNode(0, head), prev = dummy, curr = head.
  2. For each curr: scan temp from curr.next. Flag if any temp.val > curr.val.
  3. Flag → prev.next = curr.next. No flag → prev = curr.
  4. curr = curr.next. At the end return dummy.next.

6Code (Python)

Brute force 2: nodes + prev + flag (TLE)
class Solution:
    def removeNodes(self, head):
        dummy = ListNode(0, head)        # node before the head
        prev = dummy
        curr = head
        while curr:
            temp = curr.next
            delete = False
            while temp:
                if temp.val > curr.val:  # something bigger to the right
                    delete = True
                    break
                temp = temp.next
            if delete:
                prev.next = curr.next    # cut curr out
            else:
                prev = curr              # curr stays, it is the new safe prev
            curr = curr.next
        return dummy.next

7Code line by line

linewhat it means
dummy = ListNode(0, head)A fake node in front, so even the head has a "previous".
prev = dummyprev = the last node we are sure we keep.
temp = curr.nextStart the scan just after curr.
if temp.val > curr.val: delete = True; breakFound a bigger node to the right. curr must go.
temp = temp.nextNot bigger, keep looking (move temp, not curr).
prev.next = curr.nextSkip over curr. It's deleted.
prev = currcurr survived, so it becomes the node before the next candidate.
return dummy.nextThe real head, even if the old head was deleted.

8Dry run

stepprev, currtemp scanwhat changeslist from dummy
1D, 52 ✗, 13 ✓D.next = 2 (5 cut)0 → 2 → 13 → 3 → 8
2D, 213 ✓D.next = 13 (2 cut)0 → 13 → 3 → 8
3D, 133 ✗, 8 ✗, Nonekeep → prev = 130 → 13 → 3 → 8
413, 38 ✓13.next = 8 (3 cut)0 → 13 → 8
513, 8Nonekeep → prev = 80 → 13 → 8

Snapshot during step 1, when temp finds 13:

[0] → [5] → [2] → [13] → [3] → [8] → None
 ↑     ↑            ↑
prev  curr         temp      → prev.next = curr.next

Return dummy.next → 13 → 8.

9Complexity & remember

RememberDelete = prev.next = curr.next. Move prev only when curr stays. A dummy saves you from the "delete the head" case.

Part C · Better: monotonic stack

1The question

Same question. Goal: get rid of the inner scan, so the time becomes O(n).

2What the constraints tell us

n = 10⁵ needs O(n) or O(n log n). Extra O(n) memory for 10⁵ nodes is fine.

3Intuition: this is "next greater element"

The teacher's observation: "is there a bigger value to my right?" is the next greater element question from the stack playlist. There, a stack turned O(n²) into O(n). So we use the same tool here.

One twist. In next-greater problems we usually walk from the right, because we need to print the next greater value of each element. Here we don't need that value. We only need to throw away the losers. So the teacher walks from the left: keep a stack of the nodes that survive so far. When a new node arrives, every node on top of the stack that is smaller than it has just found a bigger value to its right, so pop it (delete it).

Picture people standing in a row looking right, where a taller person who arrives later blocks everyone shorter before them. The stack holds the people still "visible". Each newcomer knocks off the shorter people at the top.

4Building the logic

Doubt: why while and not if for popping?
→ One big newcomer can beat several nodes. When 13 arrives, both 2 and 5 are smaller. An if would pop only 2 and leave 5. Keep popping until the top is not smaller (or the stack is empty).
Doubt: pop on < or on <=?
→ <. The problem removes a node only if a strictly greater value is to its right. For 5 → 5, both 5s stay.
Doubt: why does the stack stay sorted?
→ We push only after popping every smaller value, so each node sits on something greater than or equal to itself. From bottom to top the values never increase. That's why it's called a monotonic stack, and it matches the shape of the answer.

Rebuilding the list from the stack

We don't pop to rebuild (popping would give the nodes in reverse). We just loop over the stack from bottom to top, attaching each node to a tail that starts at a dummy: tail.next = node; tail = node. Return dummy.next.

Doubt: should I write tail.next = None at the end, to cut any old link?
→ The teacher first adds it, then removes it, and explains why it isn't needed. The top of the stack is always the original last node (the last node is never popped, since nothing comes after it to pop it). Its next is already None. Adding the line anyway does no harm.

5Approach steps

  1. st = [], curr = head.
  2. While curr: pop while st[-1].val < curr.val; push curr; curr = curr.next.
  3. Link the stack bottom → top with a dummy and tail. Return dummy.next.

6Code (Python)

Monotonic stack: O(n) time, O(n) space
class Solution:
    def removeNodes(self, head):
        st = []
        curr = head
        while curr:
            while st and st[-1].val < curr.val:   # smaller ones lose
                st.pop()
            st.append(curr)
            curr = curr.next

        dummy = ListNode(0)
        tail = dummy
        for node in st:                  # bottom to top = original order
            tail.next = node
            tail = node
        return dummy.next                # the last node's next is already None

7Code line by line

linewhat it means
st = []Nodes that survive so far (each still has no bigger node to its right).
while st and st[-1].val < curr.val: st.pop()curr is a bigger value to the right of the top node, so the top is deleted. Repeat. Check st first, so we never peek into an empty stack.
st.append(curr)curr survives for now. We store the node, not just the value, so we can reuse it.
for node in st:Read the stack bottom → top, without popping.
tail.next = node tail = nodeRelink the survivors in order.
return dummy.nextThe bottom of the stack (the biggest value) is the new head.

8Dry run: watch the stack

stepcurrpopsstack after (bottom → top)
15none (empty)5
225 < 2? no5, 2
3132 < 13 pop · 5 < 13 pop · empty13
4313 < 3? no13, 3
583 < 8 pop · 13 < 8? no13, 8
after step 2
52
after step 3 (13 popped 2 and 5)
13
after step 4
133
after step 5 (8 popped 3)
138

The bottom is listed first and the top is in red. Rebuild bottom → top: dummy → 13 → 8 → None ✓

9Complexity & remember

Remember"Bigger to my right?" = next greater element = monotonic stack. Walk left to right, pop while the top < curr, push curr. Survivors = the stack, bottom to top.

Part D · Optimal: reverse + "max so far"

1The question

Same question. Goal: O(n) time and no extra space (no stack).

2What the constraints tell us

O(n) passes for 10⁵. We are allowed to change the input list (we already did in Part B).

3Intuition: look from the right

The question is about what's on the right. If we stand at the right end and walk left, we can carry one number: the biggest value seen so far. Any node smaller than that max has a bigger value to its right, so delete it. Any node ≥ max stays, and becomes the new max.

But a singly linked list can't walk left. The teacher's fix: reverse the list first. Now "walking forward" in the reversed list means walking right-to-left in the original. When we're done deleting, reverse it back.

original:  [5] → [2] → [13] → [3] → [8] → None
reversed:  [8] → [3] → [13] → [2] → [5] → None
           max starts at 8 (the original last node always stays)

4Building the logic

Look at curr.next, not curr → no prev needed

Deleting normally needs a prev. The teacher avoids that: stand on curr and judge the node after it. If curr.next.val < mx, delete it with curr.next = curr.next.next. curr itself is the "previous" node.

Don't move curr after a deletion

After skipping one node, the new curr.next might also be small (in the reversed list 13 → 2 → 5, both 2 and 5 are smaller than 13). So after a delete, we stay on curr and check again. Only when curr.next survives do we move: curr = curr.next, and update mx = curr.val.

Doubt: why is mx = curr.val enough? Shouldn't it be max(mx, curr.val)?
→ We only move onto a node whose value is not smaller than mx. So its value is ≥ mx, and it really is the new max. max(...) would give the same number.

The loop condition: curr and curr.next

We read curr.next.val, so curr.next must exist. The loop runs while both exist.

The first node of the reversed list always survives

It's the original last node, which nothing can beat. So we start curr and mx there, and the reversed head never changes. We save it as head before the loop, then reverse back at the end and return the new head.

5Approach steps

  1. head = reverse(head).
  2. curr = head, mx = head.val.
  3. While curr and curr.next: if curr.next.val < mx → curr.next = curr.next.next; else → curr = curr.next, mx = curr.val.
  4. Return reverse(head).

6Code (Python)

Optimal: reverse + max so far, O(1) space
class Solution:
    def reverse(self, head):
        prev, curr = None, head
        while curr:
            nxt = curr.next
            curr.next = prev
            prev = curr
            curr = nxt
        return prev

    def removeNodes(self, head):
        head = self.reverse(head)        # now we can walk right-to-left
        curr = head
        mx = head.val                    # the original last node: always kept
        while curr and curr.next:
            if curr.next.val < mx:       # a bigger value exists to its right
                curr.next = curr.next.next   # delete it, stay on curr
            else:
                curr = curr.next         # it survives
                mx = curr.val            # and it is the new biggest
        return self.reverse(head)        # back to the original direction

7Code line by line

linewhat it means
head = self.reverse(head)Flip the list, so walking forward = walking leftward in the original.
mx = head.valThe biggest value seen so far (from the right).
while curr and curr.next:We need a next node to judge.
if curr.next.val < mx:Something bigger lies to its right (in the original), so it must go.
curr.next = curr.next.nextSkip it. Don't move curr: the new next must be checked too.
curr = curr.next mx = curr.valThe node survives, so step onto it. It is the new max.
return self.reverse(head)Restore the original order of the survivors.

8Dry run (reversed list 8 → 3 → 13 → 2 → 5)

stepcurr, mxcurr.nextwhat changesreversed list after
start8, 838 → 3 → 13 → 2 → 5
18, 83 < 88.next = 13 (3 deleted)8 → 13 → 2 → 5
28, 813 ≥ 8curr → 13, mx = 138 → 13 → 2 → 5
313, 132 < 1313.next = 5 (2 deleted)8 → 13 → 5
413, 135 < 1313.next = None (5 deleted)8 → 13
stop13Noneloop endsreverse → 13 → 8

Snapshot at step 3:

[8] → [13] → [2] → [5] → None       mx = 13
        ↑      ✗
       curr   curr.next (2 < 13 → skip it)

9Complexity & remember

Stack or reverse, which one? The teacher's rule of thumb:

Doubt: but the stack version also rewires next pointers while rebuilding. Doesn't it modify the list too?
→ True. As written it reuses the original nodes. If the input truly must stay untouched, push the values (or make new nodes with ListNode(node.val)) when rebuilding. The idea is the same. Part D, on the other hand, can't avoid changing the list, because reversing is the whole trick.
RememberReverse → walk with mx: if curr.next.val < mx, skip it (stay); else step and set mx. Reverse back.

Part E · Revision page

A · array 2 loopsB · nodes + prevC · stackD · reverse + max
ideafor each i, scan j > isame, with pointers and a dummynext greater: pop smaller topswalk from the right with a running max
timeO(n²) TLEO(n²) TLEO(n)O(n)
extra spaceO(n)O(1)O(n)O(1)
changes the list?no (new list)yesrelinks the nodesyes (reverses twice)
If you remember only 5 lines 1. Delete a node if anything strictly bigger is to its right; the last node always stays.
2. The survivors form a non-increasing list.
3. n = 10⁵ → O(n²) is TLE.
4. Stack: pop while top.val < curr.val, push curr; read the stack bottom → top.
5. No-space: reverse, keep mx, skip curr.next if it's smaller, reverse back.
Mistakes to avoid ✗ if instead of while when popping (one big node can beat many)
✗ popping on <= (equal values must stay)
✗ in Part B, moving prev after a delete
✗ in Part D, moving curr after a delete (the next one may also be small)
✗ forgetting to reverse back at the end
✗ rebuilding from the stack by popping (gives the reverse order)
test it yourself (paste under any solution)
def build(vals):
    dummy = ListNode(0)
    t = dummy
    for v in vals:
        t.next = ListNode(v)
        t = t.next
    return dummy.next

def to_list(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out

s = Solution()
print(to_list(s.removeNodes(build([5, 2, 13, 3, 8]))))   # [13, 8]
print(to_list(s.removeNodes(build([1, 1, 1, 1]))))       # [1, 1, 1, 1]
print(to_list(s.removeNodes(build([5, 4, 3, 2, 6]))))    # [6]

Based on this video: Remove Nodes From Linked List