DSA sheet · Linked List · Basic operations pattern

Delete Node in Linked List

This video teaches how to remove the node at position x from a singly linked list, and it treats the three places a node can be: the head, somewhere in the middle, and the tail. The core move is one line, curr.next = curr.next.next, and the real lesson is where to stand when you run it (one node before the target) and which None checks keep it from crashing. The teacher solves it twice: iteratively with a loop, then recursively.

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 stores a value (val) and a link to the next node (next). The last node's next is None: the end of the chain.

the node class (LeetCode style)
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val      # the number stored in this node
        self.next = next    # the next node, or None at the end
[1] → [2] → [3] → [4] → [5] → None
 ↑
head

What does "deleting" a node actually mean?

We never erase memory by hand in Python. We simply make sure no link points to that node any more. When nothing points to it, it is no longer part of the list (walking from head will never reach it), and Python's garbage collector frees it later. In C++ there's no garbage collector, so the teacher reminds C++ users to save the node in a temporary variable and delete it themselves.

Positions in this problem

Positions are counted from 1: the head is position 1, the next node is position 2, and so on. This matters for how many steps we walk.

Part A · Delete at position x, iterative

GFG · Delete in a Singly Linked List

1The question in simple words

You get the head of a singly linked list and a number x. Remove the node at position x (counting from 1) and return the head of the new list.

Example 1: x = 1 (delete the head)
before: [8] → [2] → [3] → [1] → [7] → None
after:        [2] → [3] → [1] → [7] → None
Example 2: x = 3 (delete in the middle)
before: [1] → [2] → [3] → [4] → [5] → None
after:  [1] → [2] → [4] → [5] → None

There are three situations: the node is the head, the node is in between, or the node is the last one. We'll check that one piece of code handles all three.

2What the constraints tell us

3Intuition: bypass the node

Picture the nodes as people holding hands in a line, each one holding only the hand of the person in front. To remove person 3, person 2 lets go of 3 and takes the hand of person 4 instead. Nobody holds person 3 any more, so 3 is out of the line, and the line is still unbroken.

In code, "person 2 takes person 4's hand" is: node2.next = node3.next. Since node3 is node2.next, this becomes curr.next = curr.next.next with curr standing on node 2.

4Building the logic from examples

Case 1: delete the head (x = 1). O(1), no walking

[1] → [2] → [3] → [4] → [5] → None
       ↑
   new head

Just return head.next (or move head = head.next and return head; both are the same). Why does this delete node 1? Because the list is only "what you can reach from the head". The new head is 2, and nothing points back to 1, so 1 is no longer part of the list. No loop, no walking: O(1).

Case 2: delete in the middle, e.g. x = 3

Right now, node 2 stores the address of node 3, and node 3 stores the address of node 4. To remove 3, the address that 3 was holding (node 4) must now be held by node 2.

before:  [1] → [2] → [3] → [4] → [5] → None
                ↑
               curr        curr.next = [3],  curr.next.next = [4]

after:   [1] → [2] ──────→ [4] → [5] → None
                     [3] ─┘   (nothing points to 3 any more)
Doubt 1: why stand on node 2 and not on node 3, the one we're deleting?
→ To remove a node, we must change the next of the node before it. If we stand on node 3, we have no way back to node 2 (links only go forward). So we stop one node before the target.
Doubt 2: how many jumps from the head to stand one before position x?
→ Starting on the head (position 1), reaching position x takes x − 1 jumps. We want position x − 1, so it takes x − 2 jumps. For x = 3: one jump, from node 1 to node 2. ✓ The teacher writes the loop as for i = 1; i < x - 1; "less than x − 1" is the same as "at most x − 2", so it runs x − 2 times. A while loop with a counter works too, whichever you like.

The None checks: when would curr.next = curr.next.next crash?

So we only delete if curr is not None and curr.next is not None.

Case 3: delete the tail, e.g. x = 5. Proving the code still works

[1] → [2] → [3] → [4] → [5] → None
                   ↑
                  curr     (x − 2 = 3 jumps: 1→2→3→4)

Is curr (node 4) None? No. Is curr.next (node 5) None? No. So we run 4.next = 5.next, and 5.next is None. Node 4 now points to None: node 5 is deleted, and 4 is the new tail. No special code for the tail was needed.

What if x is one past the end, e.g. x = 6?

x − 2 = 4 jumps: 1→2→3→4→5, so curr = node 5. Now curr.next is None, and without the check, curr.next.next would crash. With the check, we simply skip the deletion and return the list unchanged. This is the case the teacher uses to show why the second check is needed.

Doubt 3 (small fix): the teacher's loop jumps x − 2 times without checking curr inside the loop. Is that safe?
→ Yes, as long as x ≤ length + 1, which the problem guarantees (x is a real position). But with a bigger x (say x = 8 on 5 nodes), curr would become None partway and the next curr = curr.next would crash. Our Python code adds one line inside the loop, "if curr is None, there's nothing to delete, return head", so it never crashes. Under the given constraints it behaves exactly like the teacher's version.

5Approach steps

  1. If the list is empty, return None.
  2. If x is 1, return head.next (the head is deleted).
  3. Start curr at head and jump x − 2 times, so curr stands one node before the target.
  4. If curr and curr.next both exist, skip the target: curr.next = curr.next.next.
  5. Return head (unchanged, since the head wasn't deleted).

6Code (Python)

Delete node at position x, iterative
def deleteNode(head, x):
    if head is None:                 # empty list: nothing to delete
        return None
    if x == 1:                       # case 1: delete the head, O(1)
        return head.next

    curr = head
    for _ in range(x - 2):           # x-2 jumps: stand ONE BEFORE the target
        if curr is None:             # x is past the end (safety line)
            return head
        curr = curr.next

    if curr is not None and curr.next is not None:
        curr.next = curr.next.next   # bypass the target: this is the deletion
    return head

7Code line by line

linewhat it means
if head is None: return NoneNo list, nothing to delete.
if x == 1: return head.nextDeleting the head: the second node becomes the new head. Old node 1 is no longer reachable. No walking needed.
curr = headAn alias, so head stays on node 1 and we can return it.
for _ in range(x - 2): curr = curr.nextWalk x − 2 links. After this, curr is at position x − 1, the node just before the one to delete.
if curr is None: return headOur safety line: x was so large we walked off the list. Nothing to delete.
if curr is not None and curr.next is not None:Both checks protect curr.next.next from reading "next of None". and stops early, so if curr is None it never even looks at curr.next.
curr.next = curr.next.nextThe actual deletion: the previous node now points to the node after the target. The target is skipped, and the garbage collector frees it.
return headThe head didn't change, so return it as is.

8Dry run

List 1 → 2 → 3 → 4 → 5. Three runs: x = 3 (middle), x = 5 (tail), x = 6 (past the end).

xjumps (x − 2)curr ends oncheckswhat changeslist after
31: 1→2[2]2 ✓, 2.next = 3 ✓2.next: [3] → [4]1 → 2 → 4 → 5
53: 1→2→3→4[4]4 ✓, 4.next = 5 ✓4.next: [5] → None1 → 2 → 3 → 4
64: 1→2→3→4→5[5]5 ✓, 5.next = None ✗nothing (skip)1 → 2 → 3 → 4 → 5
1none—x == 1return head.next2 → 3 → 4 → 5

Step-by-step for x = 3:

steppointerswhat changeslist picture
1head = [1], curr = [1]start[1] → [2] → [3] → [4] → [5] → None
2curr = [2]1 jump (loop runs x − 2 = 1 time)[1] → [2] → [3] → [4] → [5] → None
3curr = [2], curr.next = [3]checks pass → 2.next = 3.next = [4][1] → [2] → [4] → [5] → None
4—return head = [1]answer: 1 → 2 → 4 → 5

Snapshot just before the rewiring, and just after:

before:  [1] → [2] → [3] → [4] → [5] → None
          ↑     ↑
        head   curr

after:   [1] → [2]   [3] → [4] → [5] → None
                 │          ↑
                 └──────────┘
         (2 now points to 4; [3] still points to [4], but nobody points to [3])

9Complexity & remember

Remember iterative delete x == 1 → return head.next. Otherwise walk x − 2 jumps to stand one before the target, check curr and curr.next are not None, then curr.next = curr.next.next. Return head.

Part B · Delete at position x, recursive

1The question (same as Part A)

Same input (head, x), same output (head of the list with node x removed). The teacher shows the recursive version because recursion was covered earlier in the playlist, and turning a loop into recursion is good practice.

2What the constraints tell us

3Intuition: each call asks "is it me?"

Each call stands on one node and holds a number x that means "the node to delete is x positions away, counting me as 1".

The things that change from one loop round to the next in Part A (where curr stands, how many jumps are left) become the parameters of the recursive call. That's the teacher's general rule for turning a loop into recursion.

4Building the logic from examples

Base cases (same as Part A)

Example: list 1 → 2 → 3 → 4 → 5, x = 2

  1. First call: head = [1], x = 2. Not None, x isn't 1. The target is further on. Call again with head.next (node 2) and x − 1 = 1.
  2. Second call: head = [2], x = 1. x is 1 → return head.next, which is node 3 (and with it 3 → 4 → 5).
  3. Back in the first call: the returned node 3 must be stored as node 1's next: head.next = (result). Now 1 → 3 → 4 → 5. Node 2 is out.
  4. The first call returns head (node 1), the full new list.
Doubt 1: why x − 1 in the next call, and not x or x + 1?
→ x is "how far the target is from the node I'm standing on". When we move one node forward, the target becomes one step closer. If we had started a separate counter at 1 we would count up until it equals x; since we are reusing x itself, we count down until it reaches 1. Either way works, but counting x down needs no extra variable.
Doubt 2: in the iterative code we used an alias curr so as not to lose head. Why don't we need one here?
→ Each recursive call is a separate function run with its own head variable, kept on the call stack. Moving forward means passing head.next to a new call; the current call's head isn't changed. So every call still remembers its own node, and the first call still has the real head to return.
Doubt 3: why head.next = deleteNode(head.next, x - 1) and not return deleteNode(head.next, x - 1)?
→ The inner call returns the list starting from the next node (with the deletion done). If we just returned it, the current node would be dropped. For x = 2 we'd return 3 → 4 → 5 and lose node 1. Instead, we attach the returned list to the current node with head.next = ..., and then return head. The teacher stresses this: don't put the recursive call in the return statement; save it in head.next.

5Approach steps

  1. If head is None → return None.
  2. If x is 1 → return head.next.
  3. Otherwise set head.next = deleteNode(head.next, x - 1).
  4. Return head.

6Code (Python)

Delete node at position x, recursive
def deleteNode(head, x):
    if head is None:            # empty / walked past the end
        return None
    if x == 1:                  # this node is the target
        return head.next        # skip it
    head.next = deleteNode(head.next, x - 1)   # fix the rest, attach it to me
    return head

7Code line by line

linewhat it means
if head is None: return NoneBase case 1. Empty list, or x was past the end (we keep calling until we fall off): nothing to delete, give back an empty tail.
if x == 1: return head.nextBase case 2. The node I'm on is the target. Return the list after it, so whoever called me links straight past me.
head.next = deleteNode(head.next, x - 1)Ask the next node to delete "position x − 1 from there". Whatever comes back (the next node itself, or the node after the target) becomes my new next.
return headI am not the target, so I stay. Return myself as the start of the fixed list.

8Dry run: x = 3 on 1 → 2 → 3 → 4 → 5

  1. Call (1, x=3): not None, x ≠ 1 → calls (2, x=2). (1, 3) waits on the stack.
  2. Call (2, x=2): x ≠ 1 → calls (3, x=1). (2, 2) waits.
  3. Call (3, x=1): x == 1 → returns 3.next = node 4. This call leaves the stack.
  4. Back in (2, x=2): 2.next = 4 → rewire: node 2 now points to 4. Returns node 2.
  5. Back in (1, x=3): 1.next = 2 (it was already 2, so nothing really changes). Returns node 1.
  6. Final list: 1 → 2 → 4 → 5.
after step 2
(1, x=3)(2, x=2)
step 3 (deepest)
(1, x=3)(2, x=2)(3, x=1) → [4]
step 4
(1, x=3)(2): 2.next = [4] → [2]
step 5
(1): 1.next = [2] → [1]

The newest call is on top (red). Calls are pushed while walking forward, and on the way back each one re-attaches what it got to its own next.

returning callreturnswhat changeslist after this step
(3, x=1)[4]nothing yet[1] → [2] → [3] → [4] → [5]
(2, x=2)[2]2.next: [3] → [4][1] → [2] → [4] → [5]
(1, x=3)[1]1.next set to [2] again (same)[1] → [2] → [4] → [5]

And with x = 1, the very first call returns head.next at once: no recursion, nothing on the stack.

9Complexity & remember

Remember recursive delete None → None · x == 1 → head.next · else head.next = deleteNode(head.next, x - 1) and return head. Attach the result, don't return it directly.

Part C · Revision page

IterativeRecursive
delete head (x = 1)return head.nextreturn head.next
moving forwardcurr = curr.next, x − 2 timescall with (head.next, x - 1)
where the deletion happenscurr.next = curr.next.next (curr is one before)the target call returns head.next; the caller stores it in its next
None safetycheck curr and curr.nexthead is None base case
alias needed?yes (curr)no, each call has its own head
time / space (position x)O(n) / O(1)O(n) / O(n) stack
time / space (head)O(1) / O(1)O(1) / O(1)
positionwhat we doexample on 1→2→3→4→5
headreturn head.nextx = 1 → 2→3→4→5
middlestand on x − 1, bypassx = 3 → 1→2→4→5
tailstand on second-last, point it to Nonex = 5 → 1→2→3→4
past the endchecks fail → unchangedx = 6 → 1→2→3→4→5
If you remember only 5 lines 1. Deleting = making nothing point to the node; the garbage collector does the rest (C++: delete it yourself).
2. Head: just return head.next, O(1).
3. Others: stand one before the target, which is x − 2 jumps from the head.
4. Check curr and curr.next are not None, then curr.next = curr.next.next.
5. Recursion: head.next = f(head.next, x - 1), return head. Same time, but O(n) stack.
Mistakes to avoid ✗ walking x − 1 jumps and landing on the target (you can't reach the previous node any more)
✗ moving head itself in the loop (you lose the start of the list)
✗ calling curr.next.next without checking curr.next (crash at the tail / past the end)
✗ return f(head.next, x - 1) in recursion (drops all nodes before the target)
✗ forgetting the x == 1 case (there's no node before the head)
test it yourself (paste under either solution above)
class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next

def build(vals):
    head = None
    for v in reversed(vals):
        head = ListNode(v, head)
    return head

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

print(to_list(deleteNode(build([8, 2, 3, 1, 7]), 1)))   # [2, 3, 1, 7]
print(to_list(deleteNode(build([1, 2, 3, 4, 5]), 3)))   # [1, 2, 4, 5]
print(to_list(deleteNode(build([1, 2, 3, 4, 5]), 5)))   # [1, 2, 3, 4]
print(to_list(deleteNode(build([1, 2, 3, 4, 5]), 6)))   # [1, 2, 3, 4, 5]

Based on this video: Delete Node in Linked List | Iterative & Recursive