Loading content...
In the previous page, we established that arrays should be your default choice for most scenarios. Their cache efficiency, lower memory overhead, and simpler implementation make them the pragmatic option when you're unsure.
But there are situations — important, common situations — where linked lists are genuinely superior. Not just marginally better, but fundamentally more appropriate for the problem at hand.
This page identifies those scenarios with precision. By the end, you'll have a clear mental checklist for recognizing when a linked list is the right tool.
You will learn the specific, concrete criteria that indicate a linked list is the optimal choice. We'll examine each criterion in depth, with real-world examples and code patterns that demonstrate when linked lists provide measurable advantages over arrays.
This is the primary scenario where linked lists excel. If your algorithm requires frequent insertions or deletions at positions other than the end, and you can maintain references to those positions, linked lists provide O(1) operations where arrays require O(n).
The critical requirement: known positions
Linked lists only provide O(1) insertion/deletion when you already have a reference (pointer) to the relevant node. This is fundamentally different from inserting at an arbitrary index.
The distinction matters enormously. Linked lists don't magically make all insertions fast — they make insertions at known locations fast.
To leverage linked list insertion speed, design your algorithms to maintain node references. Store pointers to nodes alongside other data. Pass nodes between functions, not indices. This is a different programming style, but it's what makes linked list operations efficient.
Example: LRU Cache Implementation
An LRU (Least Recently Used) cache is a classic example where this criterion applies:
With an array: Moving an item to the front requires shifting all items between its current position and the front — O(n). This is unacceptable for a cache that might handle thousands of accesses per second.
With a doubly linked list: Removing a node and inserting at the head are both O(1). Combined with a hash map for O(1) lookup, this gives us an O(1) LRU cache.
123456789101112131415161718192021222324252627282930313233343536
// LRU Cache using Doubly Linked List + Hash Map// Each node contains: key, value, prev pointer, next pointer Access(key): if key in hashMap: node = hashMap[key] // Move to front in O(1): // 1. Remove from current position (update neighbors' pointers) node.prev.next = node.next node.next.prev = node.prev // 2. Insert at head node.next = head.next node.prev = head head.next.prev = node head.next = node return node.value else: return CACHE_MISS Insert(key, value): if at capacity: // Remove from back in O(1): lru = tail.prev lru.prev.next = tail tail.prev = lru.prev delete hashMap[lru.key] // Insert at front in O(1) newNode = Node(key, value) newNode.next = head.next newNode.prev = head head.next.prev = newNode head.next = newNode hashMap[key] = newNode // All operations are O(1)!Linked lists grow and shrink efficiently without reallocation. When the collection size is unpredictable — especially wenn it fluctuates significantly — linked lists avoid the worst-case behaviors of dynamic arrays.
The dynamic array problem:
Dynamic arrays cope with size uncertainty by over-allocating. When capacity is reached, they allocate a new, larger block and copy all elements. This has consequences:
When size unpredictability matters:
Example: Stream Processing Buffer
Consider a network stream processor that buffers incoming packets:
With an array: You must choose initial capacity. Too small → frequent reallocation during bursts. Too large → wasted memory during quiet periods. Reallocation during peak traffic causes latency when you can least afford it.
With a linked list: Each packet is a new node. No reallocation during bursts. Memory usage exactly matches current buffer size. Consistent per-packet insertion time regardless of buffer state.
The trade-off: Linked lists use more total memory per element, but that memory exactly matches needs. Arrays use less memory per element but may waste capacity.
Modern memory allocators (jemalloc, tcmalloc) include optimizations for frequent small allocations, reducing linked list overhead. However, they cannot eliminate the fundamental per-node overhead. Profile in your specific environment before deciding.
This is a frequently overlooked advantage of linked lists: pointers to nodes remain valid after other modifications.
In arrays, almost any mutation can invalidate pointers and indices:
In linked lists, a pointer to a node remains valid regardless of what happens to other nodes. The node stays in the same memory location; only its links change.
arr[3] may become dangling after reallocationExample: Observer Pattern with Dynamic Subscribers
Consider an event system where observers subscribe and unsubscribe dynamically:
With an array: When a subscriber unsubscribes, removing it from the array shifts later elements. Any subscribers holding their indices (or pointers computed from indices) are now invalid. The system must either:
With a linked list: Each subscription is a node. The subscriber holds a pointer to this node. Unsubscription removes the node by updating neighbors' pointers. No other references are affected. The subscriber's pointer, though removed from the list, remains valid until explicitly freed.
For maximum efficiency, consider intrusive linked lists where the node pointers are embedded directly in the data structure. This eliminates separate node allocations and allows the data element itself to serve as its own node. Many high-performance systems (e.g., Linux kernel) use this pattern extensively.
Linked lists can merge two lists or split one list into two in O(1) time. This is impossible with arrays, which require O(n) copying for such operations.
Merge operation:
Split operation:
Note: Finding the split point still requires O(n) traversal if specified by index. The O(1) benefit applies when you already have a reference to the split node.
Example: Work Stealing Scheduler
In parallel computing, work stealing is a load-balancing technique where idle processors "steal" work from busy processors:
The stealing operation often takes half of the victim's queue:
With an array-based deque: Stealing half the queue requires copying those elements — O(n) work during a time-critical operation.
With a linked list deque: Splitting at the midpoint involves updating two pointers. The thief takes the tail half; the victim keeps the head half. O(1) after finding the midpoint.
The performance difference matters because stealing happens precisely when the system is under load. Fast stealing means better load distribution under stress.
123456789101112131415161718192021222324252627282930313233343536
// Split a doubly linked list at a given node// O(1) operation assuming 'splitNode' is given SplitAt(list, splitNode): // Create new list starting at splitNode newList = new List() newList.head = splitNode newList.tail = list.tail // Original list ends just before splitNode if splitNode.prev != null: list.tail = splitNode.prev list.tail.next = null splitNode.prev = null else: // Split at head - original list becomes empty list.head = null list.tail = null return newList // Merge two lists: O(1)Merge(list1, list2): if list1.tail != null: list1.tail.next = list2.head else: list1.head = list2.head if list2.head != null: list2.head.prev = list1.tail if list2.tail != null: list1.tail = list2.tail // list2 is now empty/invalid return list1In real-time systems, the worst-case execution time matters more than average-case. Dynamic arrays have excellent amortized performance, but their worst case — reallocation during append — is O(n).
For systems with strict latency bounds, this occasional O(n) spike may be unacceptable:
Linked lists provide bounded worst-case time:
| Operation | Dynamic Array (Worst Case) | Linked List (Worst Case) |
|---|---|---|
| Append | O(n) — reallocation and copy | O(1) — allocate single node |
| Prepend | O(n) — shift all elements | O(1) — update head pointer |
| Delete at head | O(n) — shift all elements | O(1) — update head pointer |
| Insert at known position | O(n) — shift elements | O(1) — update pointers |
Memory allocation in real-time contexts:
A common concern is that linked list operations require memory allocation, which itself can have variable latency. Real-time systems address this through:
With these techniques, linked lists provide truly bounded operation times — essential for hard real-time systems.
Example: Audio Buffer Management
A digital audio workstation manages buffers of audio samples:
With arrays: Buffer queues might resize during peak activity, causing latency spikes precisely when the system is under stress.
With pre-pooled linked lists: Queue and dequeue operations are O(1) with bounded constant. No allocation spikes. Consistent latency regardless of load.
Simply using linked lists doesn't make a system real-time. The entire design — memory management, synchronization, I/O handling — must be considered. But choosing data structures with bounded worst-case complexity is a necessary foundation.
Linked lists often serve as the underlying implementation for other abstract data types. The choice to use a linked list isn't about using linked lists directly, but about implementing something else efficiently.
Data structures commonly implemented with linked lists:
Why linked lists for these structures:
Each of these structures benefits from linked list properties:
Stacks and Queues: Operations only at endpoints — linked lists provide O(1) there. No random access needed.
Hash Table Chaining: Chain lengths vary widely. A hash table with 10,000 buckets might have chains ranging from 0 to 100 elements. Arrays would waste space on empty/short buckets or require complex sizing.
Graph Adjacency Lists: Vertex degrees vary dramatically. A social network might have users with 10 friends and users with 10,000 friends. Linked lists accommodate this naturally.
Memory Allocators: Free blocks have inherent link structure — each block can contain a pointer to the next free block at no additional memory cost.
The abstraction principle: When using a stack or queue, you don't care whether it's implemented with a linked list or array. The implementation should be chosen based on the operations' performance requirements. For many of these structures, linked lists provide the best performance profile.
Users of a Stack or Queue shouldn't need to know it's implemented with a linked list. The linked list is an implementation detail hidden behind the abstract interface. This encapsulation allows the implementation to change if requirements evolve.
Here is a practical checklist for determining whether linked lists are appropriate for your use case. If you can answer "yes" to most of these questions, a linked list is likely the right choice.
| Criteria Matched | Recommendation |
|---|---|
| 0-2 | Strongly prefer arrays |
| 3-4 | Arrays likely better, but profile if performance matters |
| 5-6 | Linked list likely appropriate |
| 7-9 | Strongly prefer linked lists |
If the criteria are borderline, implement the simpler option (usually arrays) first. Profile with realistic data sizes and access patterns. Only switch to linked lists if profiling reveals a genuine bottleneck that linked lists would solve.
This page has identified the specific scenarios where linked lists provide genuine advantages over arrays. Let's consolidate these criteria:
The mental model:
Linked lists trade random access and cache efficiency for structural flexibility. When your algorithms need that flexibility — when you're constantly rearranging, inserting, deleting, merging, and splitting — linked lists pay back the overhead many times over.
What's next:
Now we'll examine the opposite perspective: the criteria for choosing arrays. Understanding both sides of the decision completes your mental framework for data structure selection.
You now have a clear, actionable set of criteria for identifying when linked lists are the optimal choice. These aren't arbitrary rules — they're derived from the fundamental properties of linked lists. Next, we'll complete the picture with the criteria for choosing arrays.