DSA sheet · Linked List · Basic operations pattern

Design Linked List (Array → Linked List)

This is the very first coding video of the linked list playlist. The teacher builds a linked list out of an array, first with a loop and then with recursion. It looks like a small problem, but it teaches the three habits that every later linked list problem depends on: what a node really is (a value plus the address of the next node), why we never move head, and how a recursive call connects nodes on the way back. She also says clearly that she will solve every linked list problem both ways from now on, because good recursion skills pay off later in DP and backtracking.

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

Part 0 · Linked lists from scratch

Why an array is not enough

An array keeps all its items side by side in memory, in one unbroken block. Because the block is unbroken, the computer can jump straight to item number i with simple maths. That is what we call indexing: arr[3] costs O(1).

A linked list does not need one unbroken block. Each item can sit anywhere in memory. So how do we find the next item? Each item also stores the address of the next item. An array slot holds only the data; a linked list item holds data + the address of the next item.

The teacher's memory picture

She gives each node a made-up memory address, to show what is really stored:

nodelives at addressval (data)next (what it stores)meaning
first100111003"my next node lives at 1003"
second100322004"my next node lives at 2004"
third20043None"nobody comes after me"

The three nodes are scattered (1001, 1003, 2004), but they are still a chain, because each one remembers where the next one is. If you hold the first node, you can reach the second, and from the second you can reach the third.

Drawing addresses every time is painful, so from now on we draw an arrow instead of an address:

[1] → [2] → [3] → None
Doubt: do these arrows really exist in memory?
→ No. The teacher stresses this. The arrow is only our drawing of "this node stores that node's address". In Python, node.next holds a reference to the next node object, which is the same idea.

The node class

A node is a small box with two things: val (the data) and next (the link to the next node, or None if there is no next node). We use LeetCode's class:

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

To make a node we just call the class: node = ListNode(5). We pass the value. We do not pass a next node, because at the moment of creating it we don't know who comes next, so next starts as None.

(The teacher's Java code writes new Node(5). Python has no new keyword; calling the class is enough.)

Head, tail and None

Walking the list

To move from a node to the one after it, we write curr = curr.next ("go to the address stored in my next"). We repeat that until we reach None.

[1] → [2] → [3] → None
 ↑
curr          after curr = curr.next:

[1] → [2] → [3] → None
       ↑
      curr

No index access. There is no list[3]. To reach the node at position i, you must walk i steps from the head, so it costs O(i), which is O(n) in the worst case. That is the price we pay for not needing one big block of memory.

Save before you break

A node only knows its next node. If you overwrite node.next while nothing else points at the old next node, that part of the list is lost forever (you have no way to reach it). Rule: before changing a .next, save the old next in a variable if you still need it. In this problem we only write into a .next that is None, so nothing gets lost; the next page (insert in the middle) is where this rule really bites.

Recursion on lists (needed for Part B)

A recursive function calls itself on a smaller piece of the problem. For a list, "smaller piece" usually means "the rest of the list after this node" (or "the rest of the array after index i"). Every call gets its own copy of its local variables, and Python keeps the waiting calls on the call stack. When a call returns, it hands its answer to the call that is waiting just below it.

Part A · Build the list with a loop (iterative)

GFG "Array to Linked List" (the sheet calls it Design Linked List)

1The question in simple words

You are given an array, for example arr = [1, 2, 3, 4, 5]. Make a linked list holding the same values in the same order, and return its head.

arr = [1, 2, 3, 4, 5]

answer:  [1] → [2] → [3] → [4] → [5] → None
          ↑
         head  (this is what we return)

2What the constraints tell us

3Intuition: a train and a coupling worker

Think of a train. The engine is the head. To add coaches, a worker walks to the last coach and hooks a new one behind it, then steps onto the new coach, ready to hook the next one.

The engine never moves. Only the worker walks. In code, the engine is head and the worker is a second variable called curr (current).

4Building the logic from the example

Step 1: the empty case

If len(arr) == 0, there is nothing to build → return None.

Step 2: the first node

The list must start with arr[0], so head = ListNode(arr[0]). Its next is None for now.

[1] → None
 ↑
head

Step 3: hooking the second node

Where does node 2 go? Into the spot that is currently None, and that spot is exactly head.next. So head.next = ListNode(2).

[1] → [2] → None
 ↑
head

For node 3 we need to stand on node 2 and write into its next. So we must move forward one node each time.

Step 4: the trap, moving head itself

The first idea is to move head forward: head = head.next. Then head.next = ListNode(3), and so on. Let's see what that does:

[1] → [2] → [3] → None
       ↑
      head      ← nobody points at [1] any more!

We can't walk backwards in a linked list, and no variable remembers node 1. If we now return head, the list we hand back is just 2 → 3. Node 1 is lost.

Fix: leave head where it is and make a second name for the same node, curr = head (the teacher calls it an alias). We move curr, never head.

[1] → None
 ↑
head
curr        (two names, one node)

Step 5: the loop

Node 1 is already made from arr[0], so the loop starts at index 1 and goes to the end. In each round:

  1. curr.next = ListNode(arr[i]): make the new node and hook it behind curr.
  2. curr = curr.next: the worker steps onto the new last node.
Doubt 1: why does the loop start at 1 and not 0?
→ Because arr[0] was already used to make head. Starting at 0 would add the first value twice: 1 → 1 → 2 → ….
Doubt 2: can I swap the two lines (move first, then hook)?
→ No. Before hooking, curr.next is None. Moving first makes curr = None, and then curr.next = … crashes (None has no .next). Hook first, then step onto what you just hooked.
Doubt 3: after the loop, why return head and not curr?
→ The teacher answers this with the picture. After the loop, curr is on the last node, 5. A list is known only from the node you hold onwards. Returning curr gives 5 → None. Returning head gives 1, which knows 2, which knows 3, and so on, so the whole 1 → 2 → 3 → 4 → 5 comes back.

5Approach steps

  1. If the array is empty, return None.
  2. Make head from arr[0].
  3. Set curr = head (an alias, so head stays put).
  4. For each index i from 1 to the end: hook ListNode(arr[i]) to curr.next, then move curr onto it.
  5. Return head.

6Code (Python)

Array to linked list, iterative
class Solution:
    def constructLL(self, arr):
        if len(arr) == 0:              # nothing to build
            return None
        head = ListNode(arr[0])        # first node, never moved
        curr = head                    # the worker that walks
        for i in range(1, len(arr)):
            curr.next = ListNode(arr[i])   # hook a new node at the end
            curr = curr.next               # step onto it
        return head                    # the head brings the whole chain

On screen, the teacher's first run failed because she forgot the new keyword in Java. In Python that mistake can't happen; ListNode(arr[i]) already makes the object.

7Code line by line

linewhat it means
if len(arr) == 0: return NoneNo values means no nodes. None is how we say "empty list".
head = ListNode(arr[0])Make the first node. Its next is None for now.
curr = headA second name for the same node. We'll move this one, so head keeps pointing at node 1.
for i in range(1, len(arr)):Visit the remaining values, from index 1 onwards.
curr.next = ListNode(arr[i])curr is the current last node, so its next is None. Replace that None with the new node.
curr = curr.nextWalk onto the new last node so the next round hooks behind it.
return headHand back the first node. Every other node is reachable from it.

8Dry run: arr = [1, 2, 3, 4, 5]

stepioperationwhich .next changeslist after (from head)curr on
0–head = ListNode(1), curr = headnone[1] → None1
11hook 2, move1.next: None → [2][1] → [2] → None2
22hook 3, move2.next: None → [3][1] → [2] → [3] → None3
33hook 4, move3.next: None → [4][1] → … → [4] → None4
44hook 5, move4.next: None → [5][1] → … → [5] → None5
end–return headnone[1] → [2] → [3] → [4] → [5] → Nonereturns node 1

Three snapshots, drawn in full:

after step 0
[1] → None
 ↑
head
curr
after step 2 (i = 2)
[1] → [2] → [3] → None
 ↑           ↑
head        curr
after step 4 (loop done)
[1] → [2] → [3] → [4] → [5] → None
 ↑                       ↑
head                    curr

See how head never leaves node 1 while curr runs ahead. That's the whole trick.

9Complexity & remember

Remember (iterative build) Empty → None. head = ListNode(arr[0]), curr = head. Loop from index 1: hook (curr.next = ListNode(x)) then step (curr = curr.next). Return head, never curr.

Part B · Build the list with recursion

1The question in simple words

Exactly the same task: array in, head of an equal linked list out. Only the method changes.

2What the constraints tell us

3Intuition: "make my node, let someone else build the rest"

Picture a row of helpers, one per array index. Helper i does a tiny job:

  1. Make one node holding arr[i].
  2. Ask helper i + 1: "build the list for everything after me and give me its head."
  3. Hook what comes back onto my node's next, then give my node back to whoever asked me.

The last helper (past the end of the array) has nothing to build, so it hands back None, which becomes the tail's next.

4Building the logic, the way the teacher derives it

Turn the loop body into a function

Her rule of thumb: whatever sits inside the loop becomes the body of the recursive function, and the loop variable becomes a parameter. The loop used index i to know which value to put in the node, so the function takes (arr, i). We start it with build(arr, 0).

Inside one call

First line: head = ListNode(arr[i]). For i = 0 that is node 1.

In the loop we wrote curr.next = ListNode(arr[i]). Here, the node for the next index must be built by the next call, so we replace the right side with a call: head.next = build(arr, i + 1).

Doubt 1: why don't we need curr here?
→ In the loop, we needed curr because moving head would lose node 1. In recursion, every call has its own head variable. Call 0's head is node 1, call 1's head is node 2, call 2's head is node 3. Each call remembers its own node on the call stack, and when control comes back to call 0, its head is still node 1. Nothing is ever lost, so no alias is needed.

Nodes are made going down, joined coming back up

The teacher points out a surprise. As the calls go deeper, nodes 1, 2, 3, 4, 5 all get created, but none of them are connected yet. Each call is stuck on the line head.next = build(...), waiting for the answer. The links only appear when the calls start returning.

going down (nothing linked yet):

call 0: [1] → None     waiting for build(arr, 1)
call 1: [2] → None     waiting for build(arr, 2)
call 2: [3] → None     waiting for build(arr, 3)
call 3: [4] → None     waiting for build(arr, 4)
call 4: [5] → None     waiting for build(arr, 5)
call 5: i == 5 == len(arr) → stop

The base case: when to stop and what to return

If we never stop, call 4 calls build(arr, 5), and arr[5] doesn't exist. So we stop when i == len(arr).

What should that call return? Whatever it returns lands in node 5's next. Node 5 is the last node, so its next must be None. → return None.

What does each normal call return?

Call 4 now has head = [5] and has just set 5.next = None. Call 3 is waiting to set 4.next, and it needs node 5. Which variable in call 4 holds node 5? Its head. So every call ends with return head.

Doubt 2: what if I forget return head?
→ A Python function without a return gives back None. Then every head.next = build(...) stores None, so no node is ever linked, and the top call also returns None. The teacher makes the same point: without it, 3's next would just be null instead of 4.
Doubt 3: why is the base case i == len(arr) and not i == len(arr) - 1?
→ Index len(arr) - 1 is a real element (5); it still needs its node. Only one step past it is there nothing left. Stopping at len(arr) - 1 would lose the last value. As a bonus, i == len(arr) also handles an empty array: build([], 0) returns None at once.

5Approach steps

  1. Call build(arr, 0).
  2. In build(arr, i): if i == len(arr), return None.
  3. Make head = ListNode(arr[i]).
  4. Set head.next = build(arr, i + 1) (the rest of the list).
  5. Return head.

6Code (Python)

Array to linked list, recursive
class Solution:
    def constructLL(self, arr):
        return self.build(arr, 0)

    def build(self, arr, i):
        if i == len(arr):                    # past the last element
            return None                      # becomes the tail's next
        head = ListNode(arr[i])              # this call's own node
        head.next = self.build(arr, i + 1)   # the rest, built by the next call
        return head                          # give my node to the caller

7Code line by line

linewhat it means
return self.build(arr, 0)Start the helpers at index 0. Whatever call 0 returns (node 1) is our answer.
if i == len(arr): return NoneBase case. No element here, so there is no node; None is what the last node's next should be.
head = ListNode(arr[i])Make this call's node. It's not linked to anything yet.
head.next = self.build(arr, i + 1)Pause here and let the next call build everything after me. When it returns its head, store it in my next. This is the line that links the nodes, and it runs on the way back up.
return headGive my node (with the rest already hanging behind it) to the call that is waiting for me.

8Dry run: arr = [1, 2, 3, 4, 5]

"call i" means build(arr, i).

stepcallwhat happenswhich .next changeswhat this call holds nowreturns
1call 0make [1], then call 1none yet[1](waiting)
2call 1make [2], then call 2none yet[2](waiting)
3call 2make [3], then call 3none yet[3](waiting)
4call 3make [4], then call 4none yet[4](waiting)
5call 4make [5], then call 5none yet[5](waiting)
6call 5i = 5 = len → base case––None
7call 4gets None5.next = None[5] → None[5]
8call 3gets [5]4.next = [5][4] → [5] → None[4]
9call 2gets [4]3.next = [4][3] → [4] → [5] → None[3]
10call 1gets [3]2.next = [3][2] → [3] → [4] → [5] → None[2]
11call 0gets [2]1.next = [2][1] → [2] → [3] → [4] → [5] → None[1] = answer
step 6 (deepest)
call 0 head=[1]call 1 head=[2]call 2 head=[3]call 3 head=[4]call 4 head=[5]call 5 → None
step 8
call 0 head=[1]call 1 head=[2]call 2 head=[3]call 3: 4→5, return [4]
step 11
call 0: 1→2→3→4→5, return [1]

Newest call on top (red). A call leaves the stack once it returns its head to the call below it.

Snapshots of the links:

at step 6: five loose nodes
[1]   [2]   [3]   [4]   [5]
(each one's next is None)
after step 9
[1]   [2]   [3] → [4] → [5] → None
             ↑
        call 2's head
after step 11
[1] → [2] → [3] → [4] → [5] → None
 ↑
head (returned)

9Complexity & which one is better

So both are O(n) time, but the iterative version wins on space, and the teacher picks it as the better solution. She still teaches recursion on every problem, because getting comfortable with it now makes DP and backtracking much easier later.

Remember (recursive build) Base case i == len(arr) → None. Make my node, head.next = build(arr, i + 1), return head. Nodes are created going down and linked coming back up. No curr needed: each call keeps its own head.

Part C · Revision page

Iterative (loop)Recursive
who keeps the first node safe?head stays put; curr walkseach call's own head on the call stack
where the new node is attachedcurr.next = ListNode(arr[i])head.next = build(arr, i + 1)
when links are madeimmediately, front to backon the way back up, back to front
empty arrayexplicit check → Nonebase case handles it
time / extra spaceO(n) / O(1)O(n) / O(n) stack
Python with n = 105fineRecursionError by default
If you remember only 5 lines 1. A node = value + address of the next node. Arrows are just drawings of addresses.
2. The head is the list: return the head and you return everything.
3. Never move head; walk with an alias curr.
4. Loop: hook curr.next = ListNode(x), then step curr = curr.next.
5. Recursion: base None, head.next = build(rest), return head.
Mistakes to avoid ✗ moving head instead of curr (the start of the list is lost)
✗ returning curr at the end (you get only the last node)
✗ starting the loop at 0 (first value appears twice)
✗ stepping before hooking (None.next crash)
✗ forgetting return head in the recursive version
✗ using the recursive version in Python on 105 items
test it yourself (paste under either solution)
def to_list(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out

s = Solution()
print(to_list(s.constructLL([1, 2, 3, 4, 5])))   # [1, 2, 3, 4, 5]
print(to_list(s.constructLL([7])))               # [7]
print(s.constructLL([]))                         # None

Based on this video: Design Linked List | iterative & recursive