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
- Part A · Build the list with a loop (iterative)
- Part B · Build the list with recursion
- Part C · Revision page
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:
| node | lives at address | val (data) | next (what it stores) | meaning |
|---|---|---|---|---|
| first | 1001 | 1 | 1003 | "my next node lives at 1003" |
| second | 1003 | 2 | 2004 | "my next node lives at 2004" |
| third | 2004 | 3 | None | "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
→ 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:
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 endTo 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
- Head: the first node. Tail: the last node. The tail's
nextisNone, which means "the list ends here". - The head is the most important node. Everything else is reachable from it. So when a problem says "return the linked list", we return only the head, and the whole chain comes with it.
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
- Array size: 1 to 105. The teacher's usual rule: going past about 108 operations risks TLE (Time Limit Exceeded). One pass over 105 items is 105 steps, far below the limit, so a linear O(n) solution is what we aim for. (An O(n²) idea would be 1010, too slow.)
- The size is at least 1, so the array is never empty here. The teacher still writes an empty-array check first, as a habit: with no elements there can be no list, so return
None.
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:
curr.next = ListNode(arr[i]): make the new node and hook it behindcurr.curr = curr.next: the worker steps onto the new last node.
→ Because
arr[0] was already used to make head. Starting at 0 would add the first value twice: 1 → 1 → 2 → ….→ 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.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
- If the array is empty, return
None. - Make
headfromarr[0]. - Set
curr = head(an alias, so head stays put). - For each index
ifrom 1 to the end: hookListNode(arr[i])tocurr.next, then movecurronto it. - Return
head.
6Code (Python)
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 chainOn 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
| line | what it means |
|---|---|
| if len(arr) == 0: return None | No 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 = head | A 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.next | Walk onto the new last node so the next round hooks behind it. |
| return head | Hand back the first node. Every other node is reachable from it. |
8Dry run: arr = [1, 2, 3, 4, 5]
| step | i | operation | which .next changes | list after (from head) | curr on |
|---|---|---|---|---|---|
| 0 | – | head = ListNode(1), curr = head | none | [1] → None | 1 |
| 1 | 1 | hook 2, move | 1.next: None → [2] | [1] → [2] → None | 2 |
| 2 | 2 | hook 3, move | 2.next: None → [3] | [1] → [2] → [3] → None | 3 |
| 3 | 3 | hook 4, move | 3.next: None → [4] | [1] → … → [4] → None | 4 |
| 4 | 4 | hook 5, move | 4.next: None → [5] | [1] → … → [5] → None | 5 |
| end | – | return head | none | [1] → [2] → [3] → [4] → [5] → None | returns node 1 |
Three snapshots, drawn in full:
[1] → None ↑ head curr
[1] → [2] → [3] → None ↑ ↑ head curr
[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
- Time O(n): one loop, one new node per array value.
- Extra space O(1): just
head,currandi. (The n nodes themselves are the answer we were asked to build, so we don't count them as extra.)
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
- Time is still fine: one call per element, so O(n) calls.
- Python warning (my addition): each element adds one call to the call stack, so the stack gets n calls deep. Python stops at about 1000 nested calls by default (
RecursionError). With n up to 105, the recursive version would crash in Python unless you raise the limit withsys.setrecursionlimit, and even then a very deep stack can crash the program. This is one more reason the iterative version is the one to submit in Python. We learn the recursive one for the thinking.
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:
- Make one node holding
arr[i]. - Ask helper
i + 1: "build the list for everything after me and give me its head." - 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).
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.
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.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
- Call
build(arr, 0). - In
build(arr, i): ifi == len(arr), returnNone. - Make
head = ListNode(arr[i]). - Set
head.next = build(arr, i + 1)(the rest of the list). - Return
head.
6Code (Python)
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 caller7Code line by line
| line | what 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 None | Base 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 head | Give 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).
| step | call | what happens | which .next changes | what this call holds now | returns |
|---|---|---|---|---|---|
| 1 | call 0 | make [1], then call 1 | none yet | [1] | (waiting) |
| 2 | call 1 | make [2], then call 2 | none yet | [2] | (waiting) |
| 3 | call 2 | make [3], then call 3 | none yet | [3] | (waiting) |
| 4 | call 3 | make [4], then call 4 | none yet | [4] | (waiting) |
| 5 | call 4 | make [5], then call 5 | none yet | [5] | (waiting) |
| 6 | call 5 | i = 5 = len → base case | – | – | None |
| 7 | call 4 | gets None | 5.next = None | [5] → None | [5] |
| 8 | call 3 | gets [5] | 4.next = [5] | [4] → [5] → None | [4] |
| 9 | call 2 | gets [4] | 3.next = [4] | [3] → [4] → [5] → None | [3] |
| 10 | call 1 | gets [3] | 2.next = [3] | [2] → [3] → [4] → [5] → None | [2] |
| 11 | call 0 | gets [2] | 1.next = [2] | [1] → [2] → [3] → [4] → [5] → None | [1] = answer |
Newest call on top (red). A call leaves the stack once it returns its head to the call below it.
Snapshots of the links:
[1] [2] [3] [4] [5] (each one's next is None)
[1] [2] [3] → [4] → [5] → None
↑
call 2's head[1] → [2] → [3] → [4] → [5] → None ↑ head (returned)
9Complexity & which one is better
- Time O(n): one call per element. Same as the loop.
- Space O(n): the call stack holds up to n + 1 waiting calls. The loop needed only O(1) extra.
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.
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 walks | each call's own head on the call stack |
| where the new node is attached | curr.next = ListNode(arr[i]) | head.next = build(arr, i + 1) |
| when links are made | immediately, front to back | on the way back up, back to front |
| empty array | explicit check → None | base case handles it |
| time / extra space | O(n) / O(1) | O(n) / O(n) stack |
| Python with n = 105 | fine | RecursionError by default |
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.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
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([])) # NoneBased on this video: Design Linked List | iterative & recursive