A linked list is a data structure where elements (often called nodes) are connected in a sequence, with each node containing both data and a reference (or "pointer") to the next node in the sequence. A linked list is used to represent sequential data, however, unlike arrays, its objects are not contiguous in memory.
Each node has:
data
: The value being storednext
: A reference pointing to the next nodeprev
(Optional): A reference pointing to the previous node (for doubly linked lists)
The first node in the list is referred to as the head. Its prev
points to null (for doubly linked lists) denoting the start of the list. The last node in the list is referred to as the tail. Its next
points to null denoting the end of the list. For circular linked lists, there is no head or tail, since all nodes point to a next (or previous) node.
Linked lists are among the simplest data structures after arrays. While not efficient for many purposes, they excel in specialized applications like stacks, queues, and deques, since insertions and deletions next to a known element can be done in O(1) time.
An important aspect of linked lists is how they are differentiated from arrays, another data structure for storing objects sequentially.
Advantages
- Dynamic size: Can grow or shrink, whereas arrays must be reallocated.
- Efficient insertions/deletions: Inserting a node at the beginning and removing a node from the end is an O(1) operation.
- Memory efficiency: Memory is allocated as-needed. Arrays may be overprovisioned to prevent successive reallocations.
Disadvantages
- No random access: You cannot index into a linked list, as is the case with arrays. Linked lists must be traversed from the beginning requiring O(n) time complexity.
- Extra memory for pointers: Though small, the pointer to the next/prev node must be stored with its value.
- Not cache-friendly: Nodes do not share spatial locality and therefore may not be loaded from main memory into the CPU cache.
Operation | Big O |
---|---|
Access | O(n) |
Search | O(n) |
Insert† | O(1) |
Delete† | O(1) |
†: Assumes you have a reference to the insert/delete location.
There are three common types of linked lists:
- Singly linked list: Each node points to the next node and the last node points to null.
- Doubly linked list: Each node points to the next node and the previous node.
prev
of the first andnext
of the last node point to null. - Circular linked list: A singly linked list where the last node points back to the first node.
Python's built-in list
type actually uses dynamic arrays (not linked lists) under the hood. The collections.deque
class uses a doubly-linked list implementation, however.
Starting from head, update the current node to be the next node until the current node is null.
curr = head
while curr:
...
curr = curr.next
This can be done in-place by adjusting node pointers:
- Store the current node's reference to next.
- Update the current node's next to be the previous node.
- Update the previous node to be the current node.
- Update the current node to be the next node (stored in 1).
Two pointers are commonly used for finding the middle node, finding the kth node, and cycle detection.
Using two pointers, fast
is advanced by 2 and slow
is advanced by 1. When fast
reaches the end of the linked list, slow
is at the middle node.
Using two pointers, p1
is k nodes ahead of p2
. When advanced together, p2
is always k nodes behind p1
.
Using two pointers, fast
is advanced by 2 and slow
is advanced by 1. If fast
and slow
point to the same node before reaching the end of the linked list, the linked list has a cycle. This is Floyd's cycle-finding algorithm (tortoise and hare algorithm).
This can be done in-place by adjusting node pointers and using a head sentinel node: 0. Start by instantiating a head sentinel node.
- Compare the heads of the two lists.
- Update the current node's next to be the lesser of the two.
- Update the head of the list from which the node was taken.
- Update the current node in the merged list.
- If either of the lists still have nodes, attached to the end of the merged list.
Merging k sorted linked lists can be done in O(nlog k) time complexity and O(k) space complexity using a heap (priority queue).
- Build a heap using the head of each list.
- For each selection, remove the smallest element from the heap, adding it to the merged list.
- Insert the head of the list from which the node was taken into the heap.
Using a sentinel/dummy node at the head/tail of the linked list may help when operations must be performed on the head/tail. This removes the need for performing conditional checks for null pointers.