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 · Linked list basics you need for this page
- Part A · Delete at position x, iterative
- Part B · Delete at position x, recursive
- Part C · Revision page
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.
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
- head points to the first node. It is the only door into the list. A node is "in the list" only if you can reach it by starting at head and following
.nextlinks. - Walking: start a helper pointer (an alias, often called
curr) at head and repeatcurr = curr.next. We move the alias, not head, so we can still return the start of the list at the end. - No index access. There is no
list[i]. To reach position i you must walk from the head, one link at a time, so it costs O(i), up to O(n). - Save what you still need before changing a pointer. Once you overwrite a node's
next, the old next node can no longer be reached from that node. In deletion this is exactly what we want for the target node, but we must make sure the nodes after the target stay reachable. That's why we link the previous node totarget.nextin the same step.
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.
before: [8] → [2] → [3] → [1] → [7] → None after: [2] → [3] → [1] → [7] → None
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
- The list can have up to 10⁵ nodes. 10⁵ × 10⁵ = 10¹⁰ is far above about 10⁸ operations, so anything quadratic would TLE. We need a linear solution, and one walk to the position is linear.
- x is a valid position (1 ≤ x ≤ length of the list). Still, we'll write the code so that it does not crash if x is too big.
- We'll also guard against an empty list (head is
None): there is nothing to delete, so returnNone.
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)
→ 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.→ 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?
- If
currisNone, thencurr.nextmeans "next of nothing" → crash (Java calls it a NullPointerException; Python raisesAttributeError). - If
curr.nextisNone(we're standing on the last node), thencurr.next.nextagain asks for "next of nothing" → 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.
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
- If the list is empty, return
None. - If x is 1, return
head.next(the head is deleted). - Start
currat head and jump x − 2 times, socurrstands one node before the target. - If
currandcurr.nextboth exist, skip the target:curr.next = curr.next.next. - Return
head(unchanged, since the head wasn't deleted).
6Code (Python)
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 head7Code line by line
| line | what it means |
|---|---|
| if head is None: return None | No list, nothing to delete. |
| if x == 1: return head.next | Deleting the head: the second node becomes the new head. Old node 1 is no longer reachable. No walking needed. |
| curr = head | An alias, so head stays on node 1 and we can return it. |
| for _ in range(x - 2): curr = curr.next | Walk x − 2 links. After this, curr is at position x − 1, the node just before the one to delete. |
| if curr is None: return head | Our 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.next | The actual deletion: the previous node now points to the node after the target. The target is skipped, and the garbage collector frees it. |
| return head | The 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).
| x | jumps (x − 2) | curr ends on | checks | what changes | list after |
|---|---|---|---|---|---|
| 3 | 1: 1→2 | [2] | 2 ✓, 2.next = 3 ✓ | 2.next: [3] → [4] | 1 → 2 → 4 → 5 |
| 5 | 3: 1→2→3→4 | [4] | 4 ✓, 4.next = 5 ✓ | 4.next: [5] → None | 1 → 2 → 3 → 4 |
| 6 | 4: 1→2→3→4→5 | [5] | 5 ✓, 5.next = None ✗ | nothing (skip) | 1 → 2 → 3 → 4 → 5 |
| 1 | none | — | x == 1 | return head.next | 2 → 3 → 4 → 5 |
Step-by-step for x = 3:
| step | pointers | what changes | list picture |
|---|---|---|---|
| 1 | head = [1], curr = [1] | start | [1] → [2] → [3] → [4] → [5] → None |
| 2 | curr = [2] | 1 jump (loop runs x − 2 = 1 time) | [1] → [2] → [3] → [4] → [5] → None |
| 3 | curr = [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
- Deleting the head: O(1) time, O(1) space. No travel at all.
- Deleting at position x: O(n) time in the worst case, because we must walk up to x − 2 links to get there (no index access). O(1) space: just one alias.
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
- Up to 10⁵ nodes. Recursion makes one call per node it passes, so it can go up to about 10⁵ calls deep. That is fine in Java/C++ on GFG, but note that Python's default recursion limit is about 1000. For a very long list in Python, the iterative version (Part A) is the safe choice.
- Same rule as before: x is a valid position.
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".
- If x is 1, the node to delete is this node. Give back what comes after it (
head.next), which leaves this node out. - Otherwise, the target is further ahead. Ask the next node to handle the rest, with x − 1 (it's one position closer now). Whatever list the next call gives back becomes my
next. Then give back myself.
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)
head is None→ returnNone. (Empty list, or we walked past the end.)x == 1→ returnhead.next. This node is the one to remove.
Example: list 1 → 2 → 3 → 4 → 5, x = 2
- 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. - Second call: head = [2], x = 1. x is 1 → return
head.next, which is node 3 (and with it 3 → 4 → 5). - 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. - The first call returns
head(node 1), the full new list.
→ 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.
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.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
- If
headis None → return None. - If x is 1 → return
head.next. - Otherwise set
head.next = deleteNode(head.next, x - 1). - Return
head.
6Code (Python)
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 head7Code line by line
| line | what it means |
|---|---|
| if head is None: return None | Base 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.next | Base 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 head | I 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
- Call (1, x=3): not None, x ≠ 1 → calls (2, x=2). (1, 3) waits on the stack.
- Call (2, x=2): x ≠ 1 → calls (3, x=1). (2, 2) waits.
- Call (3, x=1): x == 1 → returns
3.next= node 4. This call leaves the stack. - Back in (2, x=2):
2.next = 4→ rewire: node 2 now points to 4. Returns node 2. - Back in (1, x=3):
1.next = 2(it was already 2, so nothing really changes). Returns node 1. - Final list: 1 → 2 → 4 → 5.
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 call | returns | what changes | list 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
- Deleting the head: O(1) time and O(1) space, because the first call returns straight away and makes no further calls.
- Deleting at position x: O(n) time, and O(n) space for the call stack (one waiting call per node passed).
- Which is better? The iterative version: same O(n) time but only O(1) space. The recursive one is for practice and understanding.
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
| Iterative | Recursive | |
|---|---|---|
| delete head (x = 1) | return head.next | return head.next |
| moving forward | curr = curr.next, x − 2 times | call with (head.next, x - 1) |
| where the deletion happens | curr.next = curr.next.next (curr is one before) | the target call returns head.next; the caller stores it in its next |
| None safety | check curr and curr.next | head 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) |
| position | what we do | example on 1→2→3→4→5 |
|---|---|---|
| head | return head.next | x = 1 → 2→3→4→5 |
| middle | stand on x − 1, bypass | x = 3 → 1→2→4→5 |
| tail | stand on second-last, point it to None | x = 5 → 1→2→3→4 |
| past the end | checks fail → unchanged | x = 6 → 1→2→3→4→5 |
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.✗ 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)
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