Introduction to Linked Lists: Traversal With a Current Pointer
Learn linked list traversal from scratch — how a current pointer walks the chain node by node, why there is no random access, with JavaScript code and complexity.
This article has an interactive companion
Don't just read it — step through it interactively, one state change at a time.
Every linked list algorithm you will ever write — reversing a list, finding its middle, detecting a cycle — rests on one motion: following .next pointers from the head until you fall off the end. Get that walk into your fingers and the rest of the topic stops being scary.
The difference from an array is the whole point. An array hands you arr[7] in one step because its elements sit in one contiguous block of memory. A linked list scatters its nodes anywhere on the heap and stitches them together with pointers, so the only way to reach the 7th node is to start at the head and hop six times. No shortcuts, no indexing.
This post teaches that hop — the single curr = curr.next loop that underpins everything else.
The problem
A singly linked list is a chain of nodes. Each node holds a val and a next pointer to the following node. The last node's next is null, which marks the end of the chain. You are given head, the first node. Return every value, in order, as an array.
Input: head -> 10 -> 20 -> 30 -> null
Output: [10, 20, 30]There is no .length, no head[2], no way to peek at the tail. All you have is a reference to the first node and the promise that each node points to the next one until null ends the line.
The brute force baseline
Here is a tempting non-solution: convert the list to an array by "indexing" into it, the way you would with a real array.
function traverse(head) {
const values = [];
for (let i = 0; i < length(head); i++) {
values.push(getNode(head, i).val); // getNode walks from head every time
}
return values;
}This works, but look at what getNode(head, i) has to do: with no random access, it starts at the head and follows i pointers to reach node i. Reading node 0 costs 1 hop, node 1 costs 2 hops, node 2 costs 3 hops, and so on. For an n-node list that is 1 + 2 + ... + n, which is O(n²) total — you re-walk the whole prefix on every single read. Computing length(head) first is another full pass. Treating a linked list like an array is the classic beginner trap that turns a linear job into a quadratic one.
The key insight
You never need to restart from the head. Keep a pointer that remembers where you are and move it forward one node at a time.
The reframe: instead of asking "give me node i," ask "what is the node after the one I'm holding?" That question is answered by a single field read — curr.next — in O(1). Chain those hops together and one clean pass visits every node exactly once.
The pattern is pointer traversal: a walker variable seeded at head, advanced by curr = curr.next, and stopped the instant it becomes null. That null at the tail is not an accident — it is a sentinel that tells you unmistakably when the list is over, so there is no off-by-one guessing about where to stop.
The optimal solution
This is exactly the algorithm the visualizer steps through, line for line.
function traverse(head) {
let curr = head;
const values = [];
while (curr !== null) {
values.push(curr.val);
curr = curr.next;
}
return values;
}Three moves carry the whole thing:
let curr = head— a separate pointer to walk with. We never moveheaditself, so we keep a reference to the start of the list (many later algorithms need it).while (curr !== null)— the guard. As long ascurrpoints at a real node, there is work to do. Whencurrlands onnull, the chain is exhausted and the loop ends.values.push(curr.val)thencurr = curr.next— read the value, then hop. The push is the actual "work"; the hop is the bookkeeping that gets us to the next node.
Order matters inside the loop: read before you advance. Advance first and you would skip the head's value and eventually dereference null.val, which throws.
Walkthrough
Trace head -> 10 -> 20 -> 30 -> null. Watch curr slide down the chain while values fills up.
| Step | Line | curr | curr !== null? | Action | values |
|---|---|---|---|---|---|
| 1 | let curr = head | Node(10) | — | seed the walker at the head | [] |
| 2 | while guard | Node(10) | true | node exists → enter loop | [] |
| 3 | push / advance | Node(10)→Node(20) | — | push 10, then hop to next | [10] |
| 4 | while guard | Node(20) | true | node exists → enter loop | [10] |
| 5 | push / advance | Node(20)→Node(30) | — | push 20, then hop | [10, 20] |
| 6 | while guard | Node(30) | true | node exists → enter loop | [10, 20] |
| 7 | push / advance | Node(30)→null | — | push 30, then hop to null | [10, 20, 30] |
| 8 | while guard | null | false | sentinel hit → exit loop | [10, 20, 30] |
| 9 | return | null | — | return the collected values | [10, 20, 30] |
Step 8 is the one to internalize. curr is null — it has "derailed" off the end of the list — so the guard is false and the loop stops cleanly. Every node was visited exactly once, and the null sentinel made the stopping point obvious with zero off-by-one arithmetic.
Complexity
| Approach | Time | Space | Why |
|---|---|---|---|
| Index-based (re-walk per read) | O(n²) | O(1) | reaching node i costs i hops; summed over all nodes = n²/2 |
| Pointer traversal | O(n) | O(1) | one hop and one read per node, single pass |
Time is O(n): each node is touched once, with constant work per node. Space is O(1) extra — the walk itself needs only the single curr pointer regardless of list length. (The values output array is O(n), but that is the required result, not scratch space; algorithms that only need to inspect the list, like finding a length or a max, are genuinely O(1) space.)
Common mistakes
- Advancing before reading.
curr = curr.nextfollowed byvalues.push(curr.val)skips the head and crashes whencurrbecomesnull. Read, then hop. - Looping
while (curr.next !== null). This stops one node early and never processes the tail. The guard is oncurr, notcurr.next. - Moving
headinstead of a copy. Walking withhead = head.nextdestroys your reference to the start of the list. Always introduce a separatecurr. - Forgetting the empty list. If
headisnull, the guard is false on the first check, the loop never runs, and you correctly return[]. No special case needed — but only if your guard testscurr !== nulland notcurr.next. - Treating it like an array. No
head.length, nohead[i]. Reaching for indexing is what drags you back to O(n²).
Where this pattern shows up next
The curr = curr.next until null walk is the seed for nearly every linked list problem. Add a second pointer, or a count, or a reversal, and the same traversal grows into:
- Middle of the Linked List — run a second pointer at double speed so it lands on the middle when the fast one hits
null. - Reverse Linked List — the same walk, but flip each
nextpointer as you pass. - Linked List Cycle — when a list loops back on itself,
currnever reachesnull; two pointers reveal the trap. - Linked List Cycle II — extend cycle detection to pinpoint exactly where the loop begins.
You can also step through the traversal interactively to watch curr hop node by node and the collected values fill up in real time.
FAQ
Why can't I just use an index like arr[i] on a linked list?
Because a linked list has no random access. Array elements live in one contiguous memory block, so arr[i] is a single address calculation. Linked list nodes are scattered across the heap and connected only by next pointers, so reaching node i means starting at the head and following i pointers. Indexing into a list in a loop secretly re-walks the prefix every time, turning an O(n) job into O(n²).
What is the time and space complexity of traversing a linked list?
Traversal is O(n) time because each of the n nodes is visited exactly once with constant work per node. It is O(1) extra space because the walk needs only a single curr pointer no matter how long the list is. Building an output array of all the values adds O(n) space, but that is the required result rather than working memory — a traversal that merely counts or scans stays at O(1).
Why do we copy head into a curr pointer instead of moving head directly?
The head reference is your only handle on the start of the list. If you advance head itself, you lose that anchor, and many algorithms need to return to or reference the head afterward (reversing, re-linking, returning the list). Using a separate curr lets you walk the entire chain while head stays put — a habit worth forming early because harder problems depend on it.
How does the loop know when to stop?
The last node's next pointer is null, which acts as an end-of-list sentinel. The guard while (curr !== null) stays true while curr sits on a real node and flips to false the moment curr hops onto that trailing null. Because the sentinel is unmistakable, there is no off-by-one arithmetic — you stop exactly when every node has been visited, and an empty list (head === null) simply skips the loop entirely.
Make it stick: run this one yourself
Don't just read it — step through it interactively, one state change at a time.