DSA sheet · Linked List · Basic operations pattern

Search & Insert in Linked List

The second video of the basic operations pattern. The teacher shows how to search for a value in a linked list and how to insert a new node in the three possible places: at the beginning, in between, and at the end. Search and insert-at-end are solved both with a loop and with recursion. These are the moves that every harder problem is built from: walking with an alias pointer, choosing the right loop condition (curr vs curr.next), and saving the next node before you break a link.

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 (table + drawings) → ⑨ complexity & remember

Part 0 · Linked lists from scratch

What is a node?

A linked list is a chain of nodes. Each node is a small box holding two things: val (its data) and next (a link to the next node). The nodes can live anywhere in memory; each node simply remembers where the next one is. The last node's next is None, meaning "nothing after me".

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

The head is the first node. Every other node is reachable from it, so "the list" and "the head" mean the same thing: functions take the head and return the head.

Walking, and why there's no index

We move one step with curr = curr.next. There is no list[i]: to reach position i you must take i steps from the head, which costs O(i), up to O(n). Array cells sit side by side, so an array can jump straight to index i; list nodes are scattered, so we can only follow the links.

Rule 1: never move head; use an alias

The teacher repeats this in this video: the head is your only handle on the list. There is no index to fall back on, so if head moves forward and nothing else points at the first node, the front of the list is lost for good. So we make a second name for the same node, curr = head, and move curr instead.

Rule 2: save next before you change a pointer

A node knows only its own next. If you overwrite a.next and nothing else points at the old next node, that node and everything after it are unreachable. So before rewiring a .next, store the old value in a variable (the teacher calls it temp). Part D is where this matters.

Recursion on lists (Parts B and F)

A list is "one node + a smaller list" (the rest, starting at head.next). A recursive function handles the current node, then calls itself on head.next. Each call has its own head variable saved on the call stack, so the teacher says we don't need an alias in recursion: each call remembers its own head, and when the calls return, the first call still has the original head. The price is extra memory for the stack: one waiting call per node.

The teacher's tip about linked list complexity

In linked list problems, the answer is almost always linear. A brute force usually walks the list two or three times (O(2n) or O(3n)), and the optimised version walks it once, or even half of it (O(n) or O(n/2)). O(n²) solutions are rare here. With up to 105 nodes, a linear walk is safe: about 108 operations is where TLE becomes a risk, and at 109 it is certain.

Part A · Search with a loop

GFG "Search in Linked List"

1The question in simple words

You are given the head of a list and a number key. Return True if some node holds key, otherwise False.

[1] → [2] → [3] → [4] → [5] → None

key = 4 → True        key = 6 → False

GFG also passes the length n as a parameter; we don't need it, because the None at the end tells us when to stop.

2What the constraints tell us

3Intuition

Put your finger on the first node. Read its value. If it's the key, you're done. If not, slide your finger to the next node. Keep going. If your finger falls off the end (lands on None), the key isn't in the list.

4Building the logic from the examples

The alias

We'll walk the list, so we don't use head itself. curr = head.

Search for 4

At 1: is 1 the key? No → curr = curr.next. At 2: no. At 3: no. At 4: yes → return True.

Search for 6: where must the walk stop?

1, 2, 3, 4, 5: no match. Then curr = curr.next puts curr on None. If we kept going, we'd read None.val or None.next, which crashes (the teacher calls it a null pointer exception). So the loop runs only while curr is a real node: while curr is not None.

When the loop ends this way, we've checked every node without a match → return False, written after the loop.

Doubt 1: why while curr is not None and not while curr.next is not None?
→ Search has to look at every node, including the last one. With curr.next, the loop stops while standing on 5 without ever checking 5, so searching for 5 would wrongly say False. (Also, an empty list would crash, because None.next isn't allowed.) We'll see in Part E that insert-at-end wants the other condition, because there we want to stop on the last node.
Doubt 2: why is return False outside the loop and not in an else inside it?
→ One non-matching node proves nothing; the key might be further on. Only after all nodes have failed can we say False, and that moment is exactly when the loop finishes.

5Approach steps

  1. curr = head.
  2. While curr is not None: if curr.val == key, return True; else move curr = curr.next.
  3. After the loop, return False.

6Code (Python)

Search, iterative
class Solution:
    def searchKey(self, head, key):
        curr = head                    # alias: head stays where it is
        while curr is not None:        # stop before touching None
            if curr.val == key:
                return True            # found it
            curr = curr.next           # move one node ahead
        return False                   # walked off the end: not there

7Code line by line

linewhat it means
curr = headA walking pointer. head never moves.
while curr is not None:Keep going while we're standing on a real node. This is also what makes an empty list (head = None) safe: the loop doesn't run at all.
if curr.val == key: return TrueMatch found, stop immediately.
curr = curr.nextNo match here, step forward.
return FalseEvery node was checked and none matched.

8Dry run

List 1 → 2 → 3 → 4 → 5.

stepcurr onkey = 4: compareresult so farkey = 6: compareresult so far
1[1]1 ≠ 4, move–1 ≠ 6, move–
2[2]2 ≠ 4, move–2 ≠ 6, move–
3[3]3 ≠ 4, move–3 ≠ 6, move–
4[4]4 = 4return True4 ≠ 6, move–
5[5]5 ≠ 6, move–
6Noneloop condition failsreturn False
key = 4, found at step 4
[1] → [2] → [3] → [4] → [5] → None
                   ↑
                  curr  ✓
key = 6, step 6
[1] → [2] → [3] → [4] → [5] → None
                               ↑
                              curr (loop stops)

9Complexity & remember

Remember (search, loop) curr = head; while curr: match → True, else curr = curr.next; after the loop → False. Loop on curr, not curr.next, because the last node must be checked too.

Part B · Search with recursion

1The question in simple words

Same as Part A: is key anywhere in the list?

2What the constraints tell us

3Intuition

"Is the key in this list?" can be split into two smaller questions: is it in this node? If not, is it in the rest of the list? The rest of the list is just another list, starting at head.next, so we ask the same function about it.

4Building the logic

No alias needed

In the loop, curr = curr.next moved us. In recursion, we "move" by passing head.next as the argument of the next call. Each call has its own head, so the original head is never disturbed.

The match case

if head.val == key: return True. Otherwise return searchKey(head.next, key). The key stays the same; only the starting node moves.

The base case: the loop condition, flipped

Search for 6: calls on 1, 2, 3, 4, 5, then a call on 5.next, which is None. That call must not read .val (crash). The teacher's way to find the base case: the loop ran while curr was not None, so the recursion stops when head is None. Reaching None means we went past the end without a match → return False.

Doubt 1: does the order of the two ifs matter?
→ Yes. The None check must come first. If we compared head.val first, the call on None would crash before it ever reached the base case.
Doubt 2: why return searchKey(...) and not just searchKey(...)?
→ The answer is found deep down (True at node 4, or False at None). Every waiting call must pass that answer up to its caller. Without return, the middle calls would drop it and return None.

5Approach steps

  1. If head is None → return False.
  2. If head.val == key → return True.
  3. Otherwise → return the answer for head.next.

6Code (Python)

Search, recursive
class Solution:
    def searchKey(self, head, key):
        if head is None:                       # walked past the end
            return False
        if head.val == key:                    # found in this node
            return True
        return self.searchKey(head.next, key)  # ask about the rest

7Code line by line

linewhat it means
if head is None: return FalseBase case. Empty list or past the last node: the key isn't here. This also stops the recursion.
if head.val == key: return TrueThis node has it (safe to read .val; we know head isn't None).
return self.searchKey(head.next, key)Same question on the smaller list; whatever comes back is passed straight up.

8Dry run

List 1 → 2 → 3 → 4 → 5. "f(x)" = a call whose head is node x.

stepkey = 4key = 6
1f(1): 1 ≠ 4 → call f(2)f(1): 1 ≠ 6 → call f(2)
2f(2): → call f(3)f(2): → call f(3)
3f(3): → call f(4)f(3): → call f(4)
4f(4): 4 = 4 → Truef(4): → call f(5)
5True passes up through f(3), f(2), f(1)f(5): → call f(None)
6final: Truef(None): base case → False
7False passes up through f(5) … f(1) → final False
key = 4, deepest moment
f(1)f(2)f(3)f(4) → True
key = 6, deepest moment
f(1)f(2)f(3)f(4)f(5)f(None) → False

Newest call on top (red). Each call returns the same True/False it received, and leaves the stack.

9Complexity & remember

Remember (search, recursion) None → False (first!) · match → True · else return searchKey(head.next, key). The base case is the loop's condition turned around.

Part C · Insert at the beginning

The teacher lists three kinds of insertion: at the beginning, in between, and at the end. She explains the first two on the board (with a loop), then codes the third both ways.

1The question in simple words

Add a new node with value x in front of the current first node, and return the new head.

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

2What the constraints tell us

3Intuition

To join a queue at the front, you stand in front of the first person and point at them ("you're after me"). Then everyone agrees you are now the front.

4Building the logic

  1. Make the node: node = ListNode(x). Right now it's alone: [5] → None.
  2. Point it at the old first node: node.next = head. Now [5] → [1] → [2] → ….
  3. But if we return head now, we return node 1, and the list we hand back is 1 → 2 → 3 → 4. Node 5 isn't included, because nothing points to 5 and we can't walk backwards. So, last step: make the new node the head: head = node, then return it.
Doubt: doesn't this break Rule 1 ("never move head")?
→ No. The rule is about not losing nodes. Here the new head points to the old head, so every node is still reachable from it. We're moving head backwards onto a node that already holds the whole chain, which is safe.
Doubt: can I do step 3 before step 2?
→ If you write head = node first, then node.next = head makes the node point at itself, and the old list is lost. Link first, then move head.

5Approach steps

  1. Make the new node.
  2. Set its next to the current head.
  3. Return the new node as the head.

6Code (Python)

Insert at the beginning
class Solution:
    def insertAtBeginning(self, head, x):
        node = ListNode(x)      # the new node, alone for now
        node.next = head        # link it in front of the old first node
        head = node             # the new node is now the head
        return head

7Code line by line

linewhat it means
node = ListNode(x)Make the box. Its next is None.
node.next = headThe new box points at the old first node, so it now carries the whole old list behind it. If the list was empty, head is None and nothing changes.
head = node return headReturn the new box. Returning the old head would leave the new node out.

8Dry run: insert 5 into 1 → 2 → 3 → 4

steppointerswhich .next changeslist from the returned node
1node = [5], head = [1]nonehead gives 1 → 2 → 3 → 4
2same5.next: None → [1]node gives 5 → 1 → 2 → 3 → 4
3head = [5]none5 → 1 → 2 → 3 → 4
after step 2:          after step 3:
[5] → [1] → [2] …      [5] → [1] → [2] …
 ↑     ↑                ↑
node  head             head

9Complexity & remember

Remember (insert at head)node.next = head, then head = node, return head. Link first, then move.

Part D · Insert in between

1The question in simple words

Insert a new node somewhere inside the list. The teacher says the position can be described in two ways, and both are solved the same way:

before:          [1] → [2] → [3] → [4] → None
insert 5 after 2 nodes (or after value 2):
                 [1] → [2] → [5] → [3] → [4] → None

2What the constraints tell us

3Intuition

To add a link in the middle of a chain: hold the far half in one hand first, then open the link, hook in the new piece, and connect the new piece to the far half. If you open the link without holding the far half, it falls away.

4Building the logic

Walking to the spot with a counter

We want curr to stand on the k-th node (node 2 for k = 2), because the new node goes right after it.

The teacher first writes the loop as while count <= 2, then checks it by hand and corrects it. Start with curr = head and count = 1 (we're already standing on node number 1):

With <=, the loop would run once more and stop on node 3, one node too far. So the condition is count < k with count starting at 1. We only need to move k − 1 times.

The wrong way to hook in the new node

curr is on 2. If we write curr.next = node straight away:

[1] → [2] → [5] → None

      [3] → [4] → None      ← nothing points here any more: LOST

Printing the list now gives 1 → 2 → 5. 3 and 4 are gone. The teacher's warning: don't break a link until the nodes after it are saved in another variable.

The right way: save, link, reconnect

  1. temp = curr.next: hold node 3 (and everything after it).
  2. curr.next = node: 2 now points to 5.
  3. node.next = temp: 5 now points to 3.
[1] → [2] → [5] → [3] → [4] → None
       ↑     ↑     ↑
     curr  node  temp
Doubt 1: can I avoid temp?
→ Yes, by changing the order: node.next = curr.next first (5 grabs 3), then curr.next = node. The new node itself does the "holding". Same result. The temp version is just easier to see, and it is the habit you need later in reversal, where you really do need a saved next.
Doubt 2: what about "insert after the value 2"?
→ The teacher says: same thing, just walk until curr.val equals the target instead of counting. The save–link–reconnect part is identical. If the value never appears, curr reaches None and we change nothing.

5Approach steps

  1. If k is 0, insert at the beginning (Part C).
  2. curr = head, count = 1. While count < k and curr exists: move curr, count += 1.
  3. If curr is None, the position doesn't exist: return head unchanged.
  4. temp = curr.next → curr.next = node → node.next = temp.
  5. Return head (the head didn't change).

6Code (Python)

Insert in between, after k nodes / after a value
class Solution:
    def insertAfterK(self, head, k, x):
        if k == 0:                        # before everything = insert at head
            node = ListNode(x)
            node.next = head
            return node
        curr = head
        count = 1                         # we're standing on node number 1
        while curr is not None and count < k:
            curr = curr.next              # walk to the k-th node
            count += 1
        if curr is None:                  # list shorter than k: no such spot
            return head
        node = ListNode(x)
        temp = curr.next                  # 1. save the rest of the list
        curr.next = node                  # 2. k-th node -> new node
        node.next = temp                  # 3. new node -> the saved rest
        return head

    def insertAfterValue(self, head, target, x):
        curr = head
        while curr is not None and curr.val != target:
            curr = curr.next              # walk until we stand on target
        if curr is None:                  # target not in the list
            return head
        node = ListNode(x)
        temp = curr.next
        curr.next = node
        node.next = temp
        return head

The k = 0 branch and the "list too short" check are my additions; the teacher's board version assumes a valid position.

7Code line by line

linewhat it means
if k == 0: …Nothing comes before the new node, so this is insert-at-head and the head changes.
curr = head count = 1Alias for walking. Count 1 = we're on the 1st node.
while curr is not None and count < k:Take k − 1 steps, but stop if we fall off the end.
if curr is None: return headThe list has fewer than k nodes. Leave it alone.
temp = curr.nextSave the node after the spot (could be None if k = length; that's fine).
curr.next = nodeLink: the k-th node now points to the new node. Safe, because the rest is saved.
node.next = tempReconnect: the new node points to the saved rest.
return headThe first node didn't change, so return the same head.
insertAfterValue: while … curr.val != targetSame walk, but the stopping rule is "standing on the target value" instead of a count.

8Dry run: insert 5 after k = 2 nodes in 1 → 2 → 3 → 4

steppointerswhat changeslist after this step
1curr = [1], count = 11 < 2 → move1 → 2 → 3 → 4
2curr = [2], count = 22 < 2 is false → stop1 → 2 → 3 → 4
3temp = [3]nothing rewired yet1 → 2 → 3 → 4
4curr = [2], node = [5]2.next: [3] → [5]from head: 1 → 2 → 5 (3, 4 held by temp)
5node = [5], temp = [3]5.next: None → [3]1 → 2 → 5 → 3 → 4
after step 3
[1] → [2] → [3] → [4] → None
       ↑     ↑
     curr  temp        [5] → None
                        ↑
                       node
after step 4
[1] → [2] → [5] → None
       ↑     ↑
     curr  node

            [3] → [4] → None
             ↑
            temp (still safe)
after step 5
[1] → [2] → [5] → [3] → [4] → None
 ↑
head (returned)

9Complexity & remember

Remember (insert in between) count = 1, while count < k → stand on the k-th node. Then save, link, reconnect: temp = curr.next, curr.next = node, node.next = temp.

Part E · Insert at the end with a loop

GFG "Linked List Insertion At End"

1The question in simple words

Add a node with value x after the current last node. Return the head.

before:  [1] → [2] → [3] → [4] → None
after:   [1] → [2] → [3] → [4] → [5] → None

2What the constraints tell us

3Intuition

Walk to the last coach of the train (the one with nothing behind it) and hook the new coach onto it.

4Building the logic

Where should curr stop?

The new node has to go into the last node's next. So we want curr to stop on node 4, not on the None after it. From None we can't attach anything (we've walked past the node whose next we need to change).

How do we recognise the last node? It's the one whose next is None. So: while curr.next is not None: curr = curr.next.

Trace: at 1, next is 2 → move. At 2 → move. At 3 → move. At 4, next is None → stop. Now curr.next = node, and return head.

Doubt 1: search used while curr, and this uses while curr.next. How do I choose?
→ Ask "where do I need to be standing when the loop ends?"
• Need to look at every node (search, count) → while curr; the loop ends on None.
• Need to change the last node (insert at end) → while curr.next; the loop ends on the last node.
Doubt 2: why not just move head to the end?
→ Then head would end up on 4 and returning it would give 4 → 5. Nodes 1–3 would be lost. Walk with curr.
Doubt 3: why is the empty-list check needed?
→ With head = None, curr = None and the very first curr.next crashes. And logically, if the list is empty, the new node is the first node and the last node at once, so we return the new node.

5Approach steps

  1. Make the new node.
  2. If head is None, return the new node.
  3. curr = head; while curr.next exists, move curr.
  4. curr.next = node; return head.

6Code (Python)

Insert at end, iterative
class Solution:
    def insertAtEnd(self, head, x):
        node = ListNode(x)
        if head is None:               # empty list: new node is the list
            return node
        curr = head
        while curr.next is not None:   # stop ON the last node
            curr = curr.next
        curr.next = node               # last node -> new node
        return head

7Code line by line

linewhat it means
node = ListNode(x)The new last node. Its next is already None, which is right for a tail.
if head is None: return nodeZero nodes (allowed by the constraints): the new node is the head.
curr = headAlias; head stays on node 1.
while curr.next is not None: curr = curr.nextMove until the node we're on has nothing after it, i.e. it's the last node.
curr.next = nodeReplace the last node's None with the new node.
return headSame first node; the list is one longer.

8Dry run: insert 5 at the end of 1 → 2 → 3 → 4

stepcurr oncurr.nextwhat changeslist after this step
1[1][2]not None → move1 → 2 → 3 → 4
2[2][3]move1 → 2 → 3 → 4
3[3][4]move1 → 2 → 3 → 4
4[4]Noneloop stops1 → 2 → 3 → 4
5[4]–4.next: None → [5]1 → 2 → 3 → 4 → 5
start
[1] → [2] → [3] → [4] → None
 ↑
head
curr
after step 4
[1] → [2] → [3] → [4] → None
 ↑                 ↑
head              curr
after step 5
[1] → [2] → [3] → [4] → [5] → None
 ↑                 ↑     ↑
head              curr  node

Empty list: head is None → return [5] → None straight away.

9Complexity & remember

Remember (insert at end, loop) Empty → return the new node. Else while curr.next (stop on the last node), then curr.next = node, return head.

Part F · Insert at the end with recursion

The teacher asks us to try this one ourselves first, then builds it with a call stack.

1The question in simple words

Same as Part E: add x after the last node, return the head.

2What the constraints tell us

3Intuition

Each call says to the next one: "add x at the end of your list (the rest after me) and give me back that list's head. I'll hang it on my next." The call that receives an empty list knows exactly what to do: the answer is just the new node.

4Building the logic

The base case: what happens at None?

The calls go 1 → 2 → 3 → 4 → None. In the loop, the end was where we attached the node, so the recursion stops when head is None.

What should it return? The teacher first asks the class: return None? Think about where the returned value goes: into the previous node's next. Returning None would just store None again, and the new node would never be added. Instead: we're standing exactly where the new node belongs, so create it here and return it: return ListNode(x).

Catching the returned value

The call for node 4 receives node 5. It must connect them: head.next = insertAtEnd(head.next, x). So the recursive call isn't just "returned"; its result is stored in head.next.

What each normal call returns

After node 4 links to 5, the call for node 3 is waiting to set 3.next. It needs node 4 back, so every call finishes with return head. In the end, the call for node 1 returns node 1, the head of the full list.

Doubt 1: nodes 3, 2 and 1 are already linked. Why do they set head.next again?
→ The teacher's answer: every recursive call runs the same lines of code. We needed this line for node 4 (to attach 5), so it runs for every node. For 3, 2 and 1 it stores the same node that was already there (3.next was 4 and gets 4 again), so nothing changes. It's harmless re-linking.
Doubt 2: what if I write return self.insertAtEnd(head.next, x) instead of storing it?
→ Then the top call returns whatever the bottom call returned, which is just node 5. You'd get the list 5 and lose 1–4 from the answer. Store into head.next, then return head.

5Approach steps

  1. If head is None → return a new node with x.
  2. Otherwise → head.next = insertAtEnd(head.next, x).
  3. Return head.

6Code (Python)

Insert at end, recursive
class Solution:
    def insertAtEnd(self, head, x):
        if head is None:                              # the spot after the last node
            return ListNode(x)                        # create the new node right here
        head.next = self.insertAtEnd(head.next, x)    # catch the rest and hang it on me
        return head                                   # give my node back to my caller

7Code line by line

linewhat it means
if head is None: return ListNode(x)Base case: we've walked past the last node (or the list was empty). The new node belongs here, so make it and send it up.
head.next = self.insertAtEnd(head.next, x)Insert into the rest of the list, then store the rest's head in my next. For the last real node this attaches the new node; for the others it re-stores the same node.
return headPass my node up so my caller can link to it.

8Dry run: insert 5 at the end of 1 → 2 → 3 → 4

stepcallwhat happenswhich .next changesreturns
1f(1)not None → call f(2)–(waiting)
2f(2)call f(3)–(waiting)
3f(3)call f(4)–(waiting)
4f(4)call f(None)–(waiting)
5f(None)base case: make [5]–[5]
6f(4)receives [5]4.next: None → [5] (the real change)[4]
7f(3)receives [4]3.next = [4] (same as before)[3]
8f(2)receives [3]2.next = [3] (same)[2]
9f(1)receives [2]1.next = [2] (same)[1] → 1 → 2 → 3 → 4 → 5
step 5 (deepest)
f(1)f(2)f(3)f(4)f(None) → new [5]
step 6
f(1)f(2)f(3)f(4): 4→5, return [4]
step 9
f(1): return [1]
after step 6:   [1] → [2] → [3] → [4] → [5] → None
                                   ↑
                             f(4)'s head (returned to f(3))

Empty list: the very first call has head = None → returns [5]. Correct, with no extra check needed.

9Complexity & remember

Remember (insert at end, recursion) None → return ListNode(x) · head.next = insertAtEnd(head.next, x) · return head. Store the result in head.next; don't just return it.

Part G · Revision page

operationloop condition / stop pointkey lineshead changes?timespace (loop / rec)
searchwhile curr: check every nodematch → True; after loop → FalsenoO(n)O(1) / O(n)
insert at beginningno loopnode.next = head; head = nodeyesO(1)O(1)
insert after k nodescount = 1; while count < k: stand on node ktemp = curr.next; curr.next = node; node.next = tempno (unless k = 0)O(k)O(1)
insert at endwhile curr.next: stand on the last nodecurr.next = node (empty → return node)only if emptyO(n)O(1) / O(n)
search (recursive)insert at end (recursive)
base casehead is None → Falsehead is None → ListNode(x)
work in this callhead.val == key → Truenothing before the call
recursive linereturn searchKey(head.next, key)head.next = insertAtEnd(head.next, x)
after the calljust pass the answer upreturn head
If you remember only 5 lines 1. Never move head; walk with curr. In recursion, each call keeps its own head.
2. while curr to visit every node; while curr.next to stop on the last node.
3. Insert at head: link the new node to head, then make it the head.
4. Insert in between: save curr.next, link curr → new, reconnect new → saved.
5. Iterative and recursive are both O(n) time; recursion costs O(n) stack space, so the loop wins.
Mistakes to avoid ✗ reading curr.val / curr.next when curr is None
✗ searching with while curr.next (the last node never gets checked)
✗ curr.next = node before saving the old next (the rest of the list is lost)
✗ while count <= k with count starting at 1 (one node too far)
✗ returning the old head after inserting at the front
✗ forgetting the empty-list case in insert-at-end
✗ in recursion: returning None from the base case, or returning the call instead of storing it in head.next
test it yourself (paste under the matching solution)
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

s = Solution()
# with the search solution:
# print(s.searchKey(build([1, 2, 3, 4, 5]), 4))   # True
# print(s.searchKey(build([1, 2, 3, 4, 5]), 6))   # False
# with the insert-at-end solution:
# print(to_list(s.insertAtEnd(build([1, 2, 3, 4]), 5)))   # [1, 2, 3, 4, 5]
# print(to_list(s.insertAtEnd(None, 5)))                  # [5]

Based on this video: Search & Insert in Linked List | iterative & recursive