Arrays are the first structure most programmers meet. They keep items in a neat row, let you grab any item by its position in constant time, and they are built into every language as a default. But that convenience hides a real cost: adding or removing items from the front of an array is expensive because every other item must slide to a new address. When your problem involves lots of insertions at the beginning or middle, the array becomes a silent bottleneck that only shows up in production.
A linked list offers a different deal. Instead of storing items in a single contiguous block of memory, it scatters them across the heap and connects each one to the next with a pointer. You lose instant random access — to find the tenth item you must walk through the first nine — but insertions and deletions become cheap because you only rewrite a pointer or two. Choosing between them is not about which is 'better'; it is about which cost your program actually pays most often.
graph LR A["0: Apple"] --> B["1: Banana"] B --> C["2: Cherry"] C --> D["3: Date"] style A fill:#3776ab,color:#fff style B fill:#3776ab,color:#fff style C fill:#3776ab,color:#fff style D fill:#3776ab,color:#fff
How arrays work
In JavaScript, an array is an ordered collection backed by a contiguous chunk of memory. That contiguity is what makes arr[5] instant: the engine multiplies the index by the item size and jumps straight to the address. Appending with push is usually fast, but only because the engine sometimes reserves spare room at the end. When that room runs out, the entire array must be copied to a larger block, an occasional O(n) cost that average-case analysis usually smooths over.
The real pain arrives at the front. unshift must shift every existing element one slot to the right, which is O(n). If you call unshift inside a loop, you have accidentally built an O(n²) algorithm. The same logic applies to splice in the middle: every element after the insertion point must move. Arrays excel at random access and append-only workloads, but they protest loudly when you disturb their order from the front.
const arr = [1, 2, 3]; arr.push(4); console.log(arr); arr.unshift(0); console.log(arr);
Linked lists: nodes and pointers
A linked list is a chain of nodes. Each node holds two things: a value and a reference — called next — to the following node. The list itself usually keeps a reference to the head node, which is your entry point. If the list also tracks the tail, appending becomes O(1); otherwise you must walk the entire chain to reach the end.
The key operation is rewiring pointers. To insert a new node at the front, you create the node, point its next at the current head, and then update the list's head to the new node. No existing node moves in memory; only one pointer changes. Removing the front is equally cheap: move the head reference to head.next. The old first node is simply abandoned, and the garbage collector eventually reclaims it.
flowchart LR N1["Node A<br/>value: 1<br/>next"] --> N2["Node B<br/>value: 2<br/>next"] N2 --> N3["Node C<br/>value: 3<br/>next: null"] style N1 fill:#e0900b,color:#fff style N2 fill:#e0900b,color:#fff style N3 fill:#e0900b,color:#fff
function createNode(value, next = null) {
return { value, next };
}
const head = createNode(1, createNode(2, createNode(3)));
let current = head;
while (current) {
console.log(current.value);
current = current.next;
}
Measuring the difference
The gap between O(1) and O(n) is invisible on ten items but painful on ten thousand. If you are building a queue that adds and removes from opposite ends, an array will spend most of its time shifting elements, while a linked list handles each operation in constant time. The right choice is not about elegance; it is about matching the structure to the workload.
When to choose which
Arrays win when you need fast lookup by position, when you mostly append at the end, or when memory locality matters for cache performance. Because the items sit next to each other in memory, the CPU can prefetch the next item before you even ask for it, making sequential array scans surprisingly fast.
Linked lists win when you are constantly inserting or deleting at the front, when the collection grows and shrinks unpredictably, or when you cannot afford the shifting cost of a large array. They also make it easy to split or join two lists by rewiring a single pointer, something an array cannot do without copying.
In practice, JavaScript's built-in array is so well optimised that a handmade linked list is rarely faster for small data. But the conceptual difference is vital: understanding why an array is fast at index access and slow at front insertion teaches you to read the hidden costs of any data structure, which is the real skill.
flowchart TD
Q{"Need fast lookup by index?"} -->|Yes| A["Use array"]
Q -->|No| B{"Lots of front insertions?"}
B -->|Yes| C["Use linked list"]
B -->|No| D["Array is usually fine"]
style A fill:#1e7f3d,color:#fff
style C fill:#e0900b,color:#fff
function prepend(head, value) {
return { value, next: head };
}
let list = null;
list = prepend(list, 3);
list = prepend(list, 2);
list = prepend(list, 1);
console.log(list.value, list.next.value, list.next.next.value);
What does this print? Watch the index access and the unshift.
const arr = ["a", "b", "c"];
console.log(arr[1]);
arr.unshift("z");
console.log(arr[0]);
Array indices start at 0, so arr[1] is the second element.
unshift adds at the front, so the new element becomes index 0.
Write toArray(head) that takes the head of a linked list and returns an array of its values in order. If head is null, return an empty array.
function toArray(head) {
// walk the list and collect values
}
Start with an empty result array and a current pointer at head.
Loop while current is not null, pushing current.value each time.
function toArray(head) {
const result = [];
let current = head;
while (current) {
result.push(current.value);
current = current.next;
}
return result;
}
This function is meant to count the nodes in a linked list, but it gets stuck in an infinite loop. Fix it so it returns the correct count.
function countNodes(head) {
let count = 0;
let current = head;
while (current) {
count++;
}
return count;
}
The loop must advance current to the next node each iteration.
Add current = current.next inside the loop.
function countNodes(head) {
let count = 0;
let current = head;
while (current) {
count++;
current = current.next;
}
return count;
}
You need a data structure that supports adding items to the front thousands of times per second and rarely looks up by index. Which is the better choice?
Linked lists offer O(1) front insertion by rewiring a pointer, while unshift on an array is O(n) because it must shift every element.
Write lastNode(head) that returns the last node in a linked list (the one whose next is null). If the list is empty, return null.
function lastNode(head) {
// walk to the end and return the last node
}
Stop when current.next is null — that means current is the last node.
Handle the empty list by returning null if head is null.
function lastNode(head) {
let current = head;
while (current && current.next) {
current = current.next;
}
return current;
}
Recap
- Arrays store items contiguously for O(1) index access, but front insertion is O(n).
- Linked lists store scattered nodes connected by
nextpointers; front insertion is O(1). - Rewiring pointers is cheaper than shifting memory, but you lose random access.
- JavaScript arrays are optimised objects; keep them dense and avoid treating them like hash maps.
- Choose the structure whose cost profile matches the work your program actually does.
Next you will build two classic abstract data structures on top of these foundations: the stack and the queue.