The textbook says "linked list". Production says "array". Almost every linked list I have read in the wild was either wrong, slower than the equivalent array, or written by someone who had just finished a CS course and was trying to use the new toy. Linked lists are still worth understanding. They are just not worth reaching for as often as the textbook implies, and the reason has more to do with how modern hardware works than with anything in the asymptotic table.
This article is the version of the lecture I would give a junior who just learned linked lists and is excited to use them. The goal is not to bury them. The goal is to teach you when they are genuinely the right tool, and to admit honestly when they are not.
A linked list is a chain of nodes
A linked list is a sequence where each element holds the value plus a pointer to the next element. That is the whole structure. Singly linked lists have one pointer per node (next). Doubly linked lists have two (next and prev). Circular linked lists have the tail point back to the head.
That is enough code to get the data structure off the ground. Notice that prepend is O(1). To add the same element to the front of an array, you would have to shift every existing element one position over: O(n). This is the operational difference that the textbook starts with, and it is real. The question is how often that difference matters.
What linked lists are good at, on paper
The operations linked lists are objectively faster at than arrays:
- Insert at the head:
O(1)for linked list,O(n)for array. - Delete at the head:
O(1)for linked list,O(n)for array. - Insert / delete given a node reference (no search):
O(1)for doubly linked list,O(n)for array. - Splicing two lists together:
O(1)for linked lists,O(n)for arrays.
The key phrase in those bullets is "given a node reference". Once you start needing index-based access ("the 50th element") or search-then-modify ("find the element with value 42 and delete it"), the linked list loses its advantage. Index access is O(n) for a linked list and O(1) for an array, and searching is O(n) for both, but with a much higher constant for the linked list because of cache misses (more on that in a moment).
The operation table that decides things in practice
| Operation | Array | Singly Linked | Doubly Linked |
|---|---|---|---|
| Access by index | O(1) | O(n) | O(n) |
| Insert at head | O(n) | O(1) | O(1) |
| Insert at tail (with tail ptr) | O(1) amort. | O(1) | O(1) |
| Insert in middle (given node) | O(n) | O(1) | O(1) |
| Insert in middle (given index) | O(n) | O(n) | O(n) |
| Delete given node | O(n) | O(n) prev hop | O(1) |
| Search by value | O(n) | O(n) | O(n) |
| Cache-friendliness | Excellent | Poor | Poor |
| Memory overhead per element | ~0 | 1 pointer | 2 pointers |
Look at the row "Insert in middle (given index)". Both are O(n). That row is the one most code paths actually need. You almost always have an index, not a node reference. The linked list has no asymptotic advantage there. It has a worse constant because the traversal pointer-chases instead of streaming sequential memory.
Cache locality is why arrays usually win
This is the part the asymptotic table cannot show you. Modern CPUs read memory in cache lines (typically 64 bytes). When you read element 0 of an array, the CPU also pulls in elements 1 through ~15 for free. The next iteration of your loop hits the cache instead of main memory. Memory access from L1 cache is a few clock cycles. Main memory access is a few hundred. The ratio is brutal.
A linked list places nodes wherever the allocator finds room. After a few inserts and deletes, those nodes are scattered across the heap. Walking the list jumps to a fresh memory location at each step. The CPU's prefetcher cannot predict where you are going next, so almost every step costs a main-memory round trip.
In practice, traversing a linked list of 1 million nodes can be 5-10x slower than traversing an array of 1 million elements, even though both are O(n). Big-O is silent on this. The stopwatch is not.
Where linked lists are actually correct
Three real situations where I would still choose a linked list.
LRU caches. A doubly linked list of nodes plus a hash map from key to node is the canonical LRU implementation. The hash map gives you O(1) lookup; the doubly linked list gives you O(1) move-to-front and O(1) evict-tail. An array would force you to shift elements whenever you touch a key, dragging the operation back to O(n).
Free lists in allocators. Memory allocators keep a linked list of free blocks because the operations they need are pop-from-head, push-to-head, and splice. All O(1). The data structure already knows where each block lives by pointer (it does not need index access).
OS scheduler queues. The Linux kernel's older scheduler used linked lists for runqueues for the same reason: cheap insert and remove given a pointer to the task struct.
Notice the common thread: in each case, you already have a pointer to the node when you want to operate on it. You never need index access, and you never need cache-friendly traversal across the whole structure.
Why JavaScript developers should almost never reach for one
In a language with a real Array type that supports O(1) access, O(1) push/pop at the tail, and amortized O(1) push at both ends (via Array plus an offset), you almost never need a linked list. JavaScript has none of the use cases I listed above where a hand-rolled linked list would beat the engine's Array implementation. V8's Array is heavily optimized, the cache behaviour is excellent, and the API is general enough to cover queue and stack patterns trivially.
The one exception I have seen is Map-backed LRU caches, and those use a linked list under the hood specifically because of the hash-map-plus-list pattern above. Even then, you almost certainly want to import a tested LRU library rather than writing your own.
In Java, LinkedList exists, and ArrayDeque is almost always faster for queue/stack workloads. I have not used LinkedList deliberately in years.
In Python, the standard list is a dynamic array, and collections.deque is a doubly linked list of small arrays (not a node-per-element list) that gives you O(1) ends without the cache penalty. Use deque when you want a queue. Skip the textbook linked list.
Default to arrays. Reach for linked lists only when ops are pointer-driven.
My rule, stated bluntly: if your access pattern needs index lookup, sort, or scan, use an array. If your access pattern is built on pointers you already have ("move this node to the front of the list", "splice these two lists together", "evict the tail node of an LRU list"), use a linked list. Almost everything in a typical web app falls in the first bucket. Almost nothing falls in the second.
That said, learn the structure properly. Reverse a linked list in your head. Detect a cycle with Floyd's tortoise-and-hare. Merge two sorted lists in place. These show up in interviews because they are clean tests of pointer manipulation, and the patterns transfer to graph traversal, parser combinators, and a dozen other domains where you do not draw the structure as a list but the operations look the same. Knowing linked lists is part of the job. Reaching for them in production code is rarely the right move, and now you know the specific reasons why.
