AniUI Academy

Linked Lists: The Frontend-Relevant Parts

Why arrays beat linked lists for almost everything in JavaScript — and the real situations (undo history, an LRU cache) where the linked list's O(1) insertion actually earns its keep.

9 min read

Linked lists get less airtime in this course than arrays, hashing, or trees — honestly, deliberately, because in JavaScript you will reach for a hand-rolled linked list far less often than the CS-curriculum reputation of the topic suggests. This lesson is about knowing the trade-off precisely enough to recognize the rare real situations where it matters.

The shape

function createNode(value) {
  return { value, next: null };
}
 
const c = createNode(3);
const b = createNode(2);
const a = createNode(1);
a.next = b;
b.next = c;
// a -> b -> c -> null

No array, no indices — just nodes, each holding a value and a reference to the next node. To find the third element, you don't index into anything; you walk a.next.next.value.

The one real advantage: O(1) insertion, if you already have the reference

function insertAtFront(head, value) {
  const node = createNode(value);
  node.next = head;
  return node; // this is the new head
}

This is genuinely O(1) — create a node, point it at the old head, done. Compare to an array:

function insertAtFrontArray(arr, value) {
  arr.unshift(value); // O(n) — every existing element's index shifts up by one
  return arr;
}

unshift() is O(n) because array elements are stored by position — inserting at the front means every other element's index has to change. A linked list's nodes don't have positions, only pointers to each other, so inserting anywhere given a reference to the right spot is O(1) regardless of list size.

The real cost: everything else gets worse

That advantage comes with a genuine, unavoidable trade. Array indexing (arr[500]) is O(1) — direct memory access by position. Reaching the 500th node of a linked list means walking 500 .next references one at a time — O(k) to reach the k-th node, with no shortcut. And you give up the entire standard library — map, filter, reduce, sort, includes — none of it exists for a hand-rolled linked list; you'd write every operation yourself.

This is exactly why arrays are the default in essentially all JavaScript code, and linked lists are not: the O(1) insertion advantage is real, but it's rarely the actual bottleneck in typical frontend work, while losing O(1) index access and the entire array method ecosystem is a cost you'd pay on every single other operation.

Where it actually earns its keep: undo/redo

The situation where a linked list's shape is a genuinely better fit, not just a textbook example: an undo/redo history, where "current" sits somewhere in the middle of a timeline, and a new edit after undoing needs to discard everything ahead of "current" before continuing.

function makeHistoryNode(state) {
  return { state, prev: null, next: null };
}
 
function recordNewState(current, newState) {
  const node = makeHistoryNode(newState);
  node.prev = current;
  current.next = node; // discards any old "redo" chain that was here — it's simply unreferenced now
  return node;
}

Once current.next is overwritten, whatever redo chain existed there before is simply gone — nothing referenced it anymore, and it becomes eligible for garbage collection. That's an O(1) "discard everything ahead and continue" operation. An array-based equivalent would need to slice() off the discarded future states (O(n)) before pushing the new one — not a disaster at typical undo-history sizes, but a real, measurable difference, and the linked structure also just reads as the right model: a chain with a current position, exactly matching the feature's own mental model.

Notice prev in that example — this is a doubly linked list, where each node points both forward and backward. That backward pointer is what lets you move from "current" back toward earlier states without re-walking from the beginning every time, and it's the exact structure the next-but-one lesson's LRU cache is built on, for exactly the same reason: O(1) removal of an arbitrary node, given a direct reference to it, requires being able to reach its predecessor without a search.

Try it yourself
Loading playground...

What to remember

  • A linked list's real advantage is O(1) insertion/removal given a reference to the relevant spot — an array's equivalent operations at the front or middle are O(n), since positions have to shift.
  • That advantage comes at a real cost: O(k) to reach the k-th node (versus an array's O(1) index access), and none of the standard array methods exist for free.
  • Arrays are the correct default for almost all JavaScript data; reach for a linked structure specifically when the problem's own shape is "a chain with a current position and cheap insertion/removal there" — undo/redo history is the clearest real example.
  • A doubly linked list's extra prev pointer is what enables O(1) removal of an arbitrary node given only a reference to it — the exact capability the upcoming LRU cache lesson depends on.

Check yourself

4 questions · pass 3/4 to unlock Stacks and Queues in Real UI Code

up to 50
  1. 1.What is the time complexity of inserting a new node at the FRONT of a singly linked list, versus inserting a new element at the front of a JavaScript array with unshift()?

  2. 2.Despite that O(1) front-insertion advantage, why do most JavaScript codebases default to arrays rather than hand-rolled linked lists for everyday lists of data?

  3. 3.An undo/redo feature stores a linked list of past document states, with a 'current' pointer somewhere in the middle. Making a new edit after undoing several steps should discard the 'redo' states ahead of current and append the new state. Why does a linked list handle this more naturally than a plain array here?

  4. 4.What structural property distinguishes a doubly linked list from a singly linked list, and what capability does it add?

4 left to answer