Linked Lists
Reading time: 25 minutes
A linked list is a linear data structure where each element, called a node, contains data and a reference (or pointer) to the next element in the sequence. Unlike arrays, linked list elements are not stored in contiguous memory locations. Instead, nodes can be scattered throughout memory, connected only by their references to one another.
This fundamental difference gives linked lists unique trade-offs compared to arrays. You gain flexibility in insertion and deletion operations but lose the ability to access elements by index in constant time. Understanding when these trade-offs favor linked lists will help you choose the right data structure for your specific problems.
The anatomy of a node
Every linked list is built from nodes. A node is the basic building block that holds two essential pieces of information: the data itself and one or more references to other nodes.
Basic node structure
The simplest node structure looks like this:
The data property holds whatever value you want to store. It could be a number, a string, an object, or any other data type. The next property holds a reference to the next node in the sequence, or null if this is the last node.
Why references matter
In arrays, you find an element by computing its memory address from the array's starting address and the element's index. The address of element i is simply startAddress + (i * elementSize). This arithmetic works because elements are contiguous.
In linked lists, you cannot compute addresses this way. Each node only knows where the next node is. To find the fifth element, you must start at the first node and follow four next references. This is the fundamental reason linked lists have O(n) access time instead of O(1).
However, this same property makes linked lists flexible. Since nodes only need to know about their neighbors, you can insert or remove nodes by updating just a few references. No shifting of elements is required.
Singly linked lists
A singly linked list is the simplest form of linked list. Each node has exactly one reference, pointing to the next node in the sequence.
Structure and representation
A singly linked list maintains a reference to its first node, called the head. Optionally, it may also track the last node (the tail) and the total count of nodes.
The list is empty when head is null. When you add the first node, both head and tail point to it. As you add more nodes, tail advances while head stays at the beginning.
Visual representation
Consider a list containing the values 10, 20, and 30:
Each box represents a node. The first section holds the data, and the second section holds the reference to the next node. The last node's next points to null, indicating the end of the list.
Traversal
Traversing a singly linked list means visiting each node from head to tail. You start at the head and follow next references until you reach null.
Time complexity: O(n), where n is the number of nodes. You visit each node exactly once.
Space complexity: O(1) for the traversal itself (not counting the result array). You only need a single pointer variable.
Inserting at the beginning
Adding a node at the head of the list is one of the most efficient operations:
Time complexity: O(1). You perform a fixed number of operations regardless of list size.
Why it is fast: You do not need to traverse the list. You simply create a new node, point it to the current head, and update the head reference.
Inserting at the end
Adding a node at the tail is also efficient if you maintain a tail reference:
Time complexity: O(1) with a tail pointer. Without a tail pointer, you would need O(n) to find the last node.
Inserting at a specific position
Inserting at an arbitrary position requires traversing to that position first:
Time complexity: O(n) in the worst case. You may need to traverse nearly the entire list to reach the insertion point.
Key insight: Once you reach the insertion point, the actual insertion takes O(1) time. The cost is in finding the position, not in inserting.
Deleting from the beginning
Removing the head node is straightforward:
Time complexity: O(1). You update a single reference.
Deleting from the end
Removing the tail node in a singly linked list is more expensive:
Time complexity: O(n). You must traverse the entire list to find the node before the tail. This is a significant limitation of singly linked lists.
Deleting at a specific position
Deleting at an arbitrary position follows a similar pattern to insertion:
Time complexity: O(n) to find the position, O(1) to perform the deletion.
Searching for a value
To find a node containing a specific value, you traverse until you find a match:
Time complexity: O(n) in the worst case. The value might be at the end or not present at all.
Complete singly linked list implementation
Here is a complete implementation with all the operations discussed:
Doubly linked lists
A doubly linked list extends the singly linked list by adding a second reference in each node. Each node points to both the next node and the previous node.
Node structure for doubly linked lists
The additional prev pointer enables traversal in both directions and makes certain operations more efficient.
Visual representation
Consider the same values 10, 20, and 30 in a doubly linked list:
Each node has two arrows: one pointing forward to next and one pointing backward to prev. The head's prev is null, and the tail's next is null.
Advantages over singly linked lists
The extra prev pointer provides several benefits:
Bidirectional traversal: You can traverse the list in either direction. Starting from the tail, you can efficiently walk backward to the head.
O(1) tail deletion: With access to prev, you can delete the tail node without traversing the entire list. You simply update tail.prev.next to null.
Easier deletion with a node reference: If you have a reference to a node in the middle of the list, you can delete it in O(1) time without needing to traverse from the head to find the previous node.
Trade-offs
The prev pointer comes with costs:
Increased memory usage: Each node requires an additional pointer. For lists with many nodes storing small data, this overhead can be significant.
More complex insertion and deletion: You must update more pointers during modifications, increasing the chance of bugs.
Slightly slower modifications: The extra pointer updates add constant-time overhead to each operation.
Doubly linked list operations
Here is a complete implementation:
Optimized position-based access
Notice how insertAtPosition and deleteAtPosition optimize traversal by choosing to start from the head or tail based on which is closer to the target position. This cuts the average traversal time in half, though the worst case remains O(n).
Circular linked lists
A circular linked list is a variation where the last node points back to the first node instead of pointing to null. This creates a continuous loop with no defined end.
Singly circular linked list
In a singly circular linked list, the tail's next pointer references the head:
Doubly circular linked list
In a doubly circular linked list, both ends connect:
The tail's
nextpoints to the headThe head's
prevpoints to the tail
Implementation of singly circular linked list
Key difference in traversal
Notice the traversal uses a do-while loop instead of a while loop. Since there is no null to terminate the loop, you must check whether you have returned to the starting node. Starting with do-while ensures you visit the head node before checking the termination condition.
Use cases for circular linked lists
Round-robin scheduling: Operating systems use circular lists to cycle through processes, giving each a time slice before moving to the next.
Circular buffers: Audio and video streaming applications use circular buffers to continuously overwrite old data with new data.
Game turns: Multiplayer games can use circular lists to cycle through player turns indefinitely.
Playlist repeat: Music players implementing repeat functionality can model the playlist as a circular list.
Time complexity comparison
Understanding the time complexity of each operation helps you choose the right type of linked list.
Singly linked list
Operation | Time Complexity | Notes |
|---|---|---|
Insert at head | O(1) | Direct access to head |
Insert at tail | O(1) | With tail pointer; O(n) without |
Insert at position | O(n) | Must traverse to position |
Delete from head | O(1) | Direct access to head |
Delete from tail | O(n) | Must find second-to-last node |
Delete at position | O(n) | Must traverse to position |
Search | O(n) | Must traverse until found |
Access by index | O(n) | Must traverse to index |
Doubly linked list
Operation | Time Complexity | Notes |
|---|---|---|
Insert at head | O(1) | Direct access to head |
Insert at tail | O(1) | Direct access to tail |
Insert at position | O(n) | Can start from closer end |
Delete from head | O(1) | Direct access to head |
Delete from tail | O(1) | Direct access via prev pointer |
Delete at position | O(n) | Can start from closer end |
Delete given node | O(1) | With direct reference to node |
Search | O(n) | Must traverse until found |
Access by index | O(n) | Can start from closer end |
Circular linked list
Time complexities are the same as their non-circular counterparts, with the exception that you can traverse from any node to any other node by continuing in one direction.
Linked lists vs. arrays
Understanding when to use linked lists versus arrays is crucial for choosing the right data structure.
When arrays are better
Random access: If you frequently access elements by index, arrays provide O(1) access versus O(n) for linked lists.
Memory efficiency for small elements: Arrays store only the data. Linked lists add pointer overhead (8 bytes per pointer on 64-bit systems) for each element.
Cache performance: Arrays store elements contiguously, which works well with CPU cache prefetching. Linked list nodes may be scattered in memory, causing cache misses.
Simple iteration: Iterating through an array is typically faster due to memory locality, even though both are O(n).
When linked lists are better
Frequent insertions and deletions at known positions: If you have a reference to a node and need to insert or delete nearby, linked lists offer O(1) operations versus O(n) for arrays.
Unknown or highly variable size: Linked lists grow and shrink naturally without reallocation. Arrays may waste space or require expensive resizing.
No index-based access needed: If you only access elements sequentially or through references, linked lists avoid the overhead of maintaining contiguous memory.
Implementing certain abstract data types: Stacks and queues can be efficiently implemented with linked lists, especially when memory is fragmented or size is unpredictable.
Practical decision guide
Ask yourself these questions:
Do I need random access by index? If yes, use an array.
Do I frequently insert or delete in the middle? If yes, and you have references to the insertion/deletion points, consider a linked list.
Is my data size predictable? If yes and fixed, arrays are simpler. If highly variable, linked lists avoid resizing.
Am I memory-constrained with small elements? If yes, arrays waste less space on overhead.
Is cache performance critical? If yes, arrays likely perform better for sequential access.
In practice, dynamic arrays (like JavaScript's built-in arrays) are the default choice for most scenarios. They offer a good balance of flexibility and performance. Linked lists shine in specific situations where their O(1) insertion and deletion properties outweigh their access time disadvantages.
Common linked list patterns and techniques
Several patterns appear frequently when solving problems involving linked lists.
Two-pointer technique (slow and fast pointers)
The two-pointer technique uses two pointers that move at different speeds. This technique solves several problems elegantly.
Finding the middle of a list
Move one pointer one step at a time (slow) and another two steps at a time (fast). When the fast pointer reaches the end, the slow pointer is at the middle.
Detecting a cycle
If a linked list has a cycle, a fast pointer will eventually catch up to a slow pointer. If there is no cycle, the fast pointer will reach null.
Finding the start of a cycle
Once you detect a cycle, you can find where it starts. Reset one pointer to the head and move both pointers one step at a time. They will meet at the cycle's start.
Finding the kth node from the end
Move the first pointer k steps ahead. Then move both pointers one step at a time. When the first pointer reaches the end, the second pointer is k steps from the end.
Reversing a linked list
Reversing a linked list is a fundamental operation that appears in many problems.
Iterative approach
Recursive approach
Reversing a portion of the list
Sometimes you need to reverse only a portion of the list, from position m to position n.
Using a dummy (sentinel) node
A dummy node (also called a sentinel node) is a placeholder node added at the beginning of a list. It simplifies edge cases involving the head.
The dummy node technique is especially useful when:
The head might be removed
You need a reference to the node before the first node
You want to avoid special-casing empty lists
Merging two sorted lists
Combining two sorted lists into one sorted list is a common operation.
Time complexity: O(n + m), where n and m are the lengths of the two lists.
Partitioning a list
Partitioning rearranges nodes so that all nodes with values less than a given value come before nodes with values greater than or equal to that value.
Real-world applications
Linked lists appear in many real-world systems, often as building blocks for more complex data structures.
Implementation of other data structures
Stacks: A singly linked list with insertion and deletion at the head provides O(1) push and pop operations.
Queues: A singly linked list with insertion at the tail and deletion at the head provides O(1) enqueue and dequeue operations.
Deques: A doubly linked list allows O(1) insertion and deletion at both ends.
Hash table chaining: Hash tables use linked lists to handle collisions. Each bucket contains a linked list of entries that hash to the same index.
LRU cache
An LRU (Least Recently Used) cache combines a hash map with a doubly linked list. The hash map provides O(1) lookup by key. The doubly linked list tracks access order, with the most recently used item at the head and the least recently used at the tail.
This implementation provides O(1) time complexity for both get and put operations.
Browser history
Browsers implement navigation history using a data structure similar to a doubly linked list. Each page visit creates a node. The back button follows the prev pointer; the forward button follows the next pointer. When you navigate to a new page from the middle of history, all forward nodes are discarded.
Music and media playlists
Media players use linked lists to implement playlists. The play order follows the list sequence. Shuffle creates a random ordering. Repeat can be implemented by making the list circular.
Undo/redo functionality
Text editors and design tools implement undo/redo using linked lists of states. Each edit creates a new node. Undo moves backward through the list; redo moves forward. This approach works well with a doubly linked list for bidirectional navigation.
Memory allocation
Operating systems use linked lists to track free memory blocks. When memory is allocated, blocks are removed from the free list. When memory is freed, blocks are added back. This approach, called the free list, allows efficient memory management without requiring contiguous free space.
Polynomial representation
Mathematical software represents polynomials as linked lists where each node contains a coefficient and an exponent. This representation efficiently handles sparse polynomials with many zero coefficients.
Common pitfalls and how to avoid them
Working with linked lists introduces several common sources of bugs.
Null pointer exceptions
The most common bug is dereferencing a null pointer. Always check for null before accessing next or prev.
Losing references
When modifying pointers, you can accidentally lose references to nodes. Save references before updating pointers.
Forgetting to update all pointers
In doubly linked lists, you must update both next and prev pointers. Missing one creates an inconsistent state.
Off-by-one errors in traversal
When traversing to a specific position, carefully count whether you need to stop before or at the target.
Infinite loops in circular lists
Circular list traversal must explicitly check for returning to the start. Without this check, you will loop forever.
Not handling empty list edge cases
Many operations behave differently on empty lists. Always consider what happens when head is null.
Summary
Linked lists are fundamental data structures that store elements in nodes connected by references. Unlike arrays, they do not require contiguous memory, which gives them unique trade-offs in terms of performance and flexibility.
Key takeaways:
Singly linked lists use one pointer per node and support O(1) insertion and deletion at the head. They require O(n) traversal to access elements by position or to delete from the tail.
Doubly linked lists add a backward pointer, enabling O(1) deletion from the tail and O(1) deletion of any node when you have a direct reference. They use more memory and require more careful pointer management.
Circular linked lists connect the tail to the head, creating a continuous loop. They are useful for round-robin scheduling, circular buffers, and any scenario requiring cyclic iteration.
The two-pointer technique solves many linked list problems elegantly, including finding the middle, detecting cycles, and finding nodes from the end.
Dummy nodes simplify edge cases involving the head by providing a consistent node before the first real element.
Linked lists excel at frequent insertions and deletions at known positions but perform poorly for random access. Arrays are better when you need index-based access or cache-friendly sequential traversal.
Real-world applications include implementing stacks, queues, hash table chaining, LRU caches, browser history, and memory allocation.
Common pitfalls include null pointer exceptions, losing references during pointer updates, forgetting to update all pointers in doubly linked lists, and infinite loops in circular lists.
Understanding linked lists gives you a foundation for more complex data structures. Many trees, graphs, and associative containers are built on the same node-and-pointer concepts you have learned here.