4.2 Linked lists¶
Memory is a resource used by all programs, and in a complex system, free space may be scattered throughout it. An array requires contiguous storage, which may be unavailable for a very large array. This is where a linked list's flexibility helps.
A linked list is a linear data structure whose elements are node objects connected by references. Each reference stores the next node's memory address, allowing us to reach it from the current node.
This design lets nodes occupy scattered memory locations; their addresses need not be contiguous.

Figure 4-5 Linked list definition and storage layout
As shown above, the basic unit of a linked list is a node object. Each node holds a value and a reference to the next node.
- The first node is called the head, and the last is called the tail.
- The tail points to a null value:
nullin Java,nullptrin C++, andNonein Python. - In languages supporting pointers, such as C, C++, Go, and Rust, these references are expressed as pointers.
As the code below shows, each ListNode stores a reference (pointer) in addition to its value. Thus, a linked list uses more memory than an array for the same amount of data.
/* Linked list node structure */
typedef struct ListNode {
int val; // Node value
struct ListNode *next; // Pointer to the next node
} ListNode;
/* Constructor */
ListNode *newListNode(int val) {
ListNode *node;
node = (ListNode *) malloc(sizeof(ListNode));
node->val = val;
node->next = NULL;
return node;
}
4.2.1 Common linked list operations¶
1. Initializing a linked list¶
Building a linked list takes two steps: initialize the node objects, then connect them with references. Afterward, start at the head and follow next references to visit every node in order.
/* Initialize the linked list 1 -> 3 -> 2 -> 5 -> 4 */
// Initialize the nodes
ListNode* n0 = new ListNode(1);
ListNode* n1 = new ListNode(3);
ListNode* n2 = new ListNode(2);
ListNode* n3 = new ListNode(5);
ListNode* n4 = new ListNode(4);
// Link the nodes with references
n0->next = n1;
n1->next = n2;
n2->next = n3;
n3->next = n4;
/* Initialize the linked list 1 -> 3 -> 2 -> 5 -> 4 */
// Initialize the nodes
ListNode n0 = new ListNode(1);
ListNode n1 = new ListNode(3);
ListNode n2 = new ListNode(2);
ListNode n3 = new ListNode(5);
ListNode n4 = new ListNode(4);
// Link the nodes with references
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
/* Initialize the linked list 1 -> 3 -> 2 -> 5 -> 4 */
// Initialize the nodes
let n0 = ListNode(x: 1)
let n1 = ListNode(x: 3)
let n2 = ListNode(x: 2)
let n3 = ListNode(x: 5)
let n4 = ListNode(x: 4)
// Link the nodes with references
n0.next = n1
n1.next = n2
n2.next = n3
n3.next = n4
/* Initialize the linked list 1 -> 3 -> 2 -> 5 -> 4 */
// Initialize the nodes
const n0 = new ListNode(1);
const n1 = new ListNode(3);
const n2 = new ListNode(2);
const n3 = new ListNode(5);
const n4 = new ListNode(4);
// Link the nodes with references
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
/* Initialize the linked list 1 -> 3 -> 2 -> 5 -> 4 */
// Initialize the nodes
const n0 = new ListNode(1);
const n1 = new ListNode(3);
const n2 = new ListNode(2);
const n3 = new ListNode(5);
const n4 = new ListNode(4);
// Link the nodes with references
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
/* Initialize the linked list 1 -> 3 -> 2 -> 5 -> 4 */\
// Initialize the nodes
ListNode n0 = ListNode(1);
ListNode n1 = ListNode(3);
ListNode n2 = ListNode(2);
ListNode n3 = ListNode(5);
ListNode n4 = ListNode(4);
// Link the nodes with references
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
/* Initialize the linked list 1 -> 3 -> 2 -> 5 -> 4 */
// Initialize the nodes
let n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None }));
let n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None }));
let n2 = Rc::new(RefCell::new(ListNode { val: 2, next: None }));
let n3 = Rc::new(RefCell::new(ListNode { val: 5, next: None }));
let n4 = Rc::new(RefCell::new(ListNode { val: 4, next: None }));
// Link the nodes with references
n0.borrow_mut().next = Some(n1.clone());
n1.borrow_mut().next = Some(n2.clone());
n2.borrow_mut().next = Some(n3.clone());
n3.borrow_mut().next = Some(n4.clone());
/* Initialize the linked list 1 -> 3 -> 2 -> 5 -> 4 */
// Initialize the nodes
ListNode* n0 = newListNode(1);
ListNode* n1 = newListNode(3);
ListNode* n2 = newListNode(2);
ListNode* n3 = newListNode(5);
ListNode* n4 = newListNode(4);
// Link the nodes with references
n0->next = n1;
n1->next = n2;
n2->next = n3;
n3->next = n4;
Visualize execution
An array is a single variable, such as nums, containing entries nums[0], nums[1], and so on. A linked list consists of separate node objects. We usually refer to the list by its head node; for example, the list above can be called n0.
2. Inserting a node¶
Inserting a node is straightforward. As shown in Figure 4-6, inserting a new node P between adjacent nodes n0 and n1 requires changing only two references (pointers), taking \(O(1)\) time.
In contrast, inserting an array element takes \(O(n)\) time and is slower for large datasets.

Figure 4-6 Inserting a node into a linked list
Visualize execution
3. Removing a node¶
Removing a node is also straightforward: as shown in Figure 4-7, only one reference (pointer) needs to change.
Although P still points to n1 after removal, traversing the list can no longer reach P. It therefore no longer belongs to the list.

Figure 4-7 Removing a linked list node
Visualize execution
4. Accessing a node¶
Accessing a node in a linked list is relatively slow. An array provides access to any element in \(O(1)\) time, but a linked list must be traversed from its head until the desired node is reached. Accessing the \(i\)-th node requires \(i - 1\) iterations, giving \(O(n)\) time complexity.
/* Access the node at index `index` in the linked list */
pub fn access<T>(head: Rc<RefCell<ListNode<T>>>, index: i32) -> Option<Rc<RefCell<ListNode<T>>>> {
fn dfs<T>(
head: Option<&Rc<RefCell<ListNode<T>>>>,
index: i32,
) -> Option<Rc<RefCell<ListNode<T>>>> {
if index <= 0 {
return head.cloned();
}
if let Some(node) = head {
dfs(node.borrow().next.as_ref(), index - 1)
} else {
None
}
}
dfs(Some(head).as_ref(), index)
}
Visualize execution
5. Finding a node¶
Traverse the list to find a node with value target, then return its index. This is also linear search. The code is as follows:
/* Find the first node with value target in the linked list */
pub fn find<T: PartialEq>(head: Rc<RefCell<ListNode<T>>>, target: T) -> i32 {
fn find<T: PartialEq>(head: Option<&Rc<RefCell<ListNode<T>>>>, target: T, idx: i32) -> i32 {
if let Some(node) = head {
if node.borrow().val == target {
return idx;
}
return find(node.borrow().next.as_ref(), target, idx + 1);
} else {
-1
}
}
find(Some(head).as_ref(), target, 0)
}
Visualize execution
4.2.2 Arrays versus linked lists¶
Table 4-1 compares their properties and operation costs. Their contrasting storage strategies give them contrasting strengths.
Table 4-1 Comparing array and linked list efficiency
| Array | Linked list | |
|---|---|---|
| Storage | Contiguous memory | Scattered memory |
| Expansion | Fixed length | Flexible expansion |
| Memory efficiency | Less memory per element, but may waste space | More memory per element |
| Access an element | \(O(1)\) | \(O(n)\) |
| Add an element | \(O(n)\) | \(O(1)\) |
| Remove an element | \(O(n)\) | \(O(1)\) |
4.2.3 Common types of linked lists¶
Figure 4-8 shows three common types.
- Singly linked list: The ordinary linked list introduced above. Each node stores a value and a reference to the next node. The first and last nodes are the head and tail, and the tail points to
None. - Circular linked list: Connecting a singly linked list's tail to its head forms a circular list. Any node can then be treated as the head.
- Doubly linked list: Each node stores references (pointers) in both directions, to its successor and predecessor. This makes it more flexible and allows traversal in either direction, at the cost of additional memory.
/* Doubly linked list node structure */
type DoublyListNode struct {
Val int // Node value
Next *DoublyListNode // Pointer to the successor
Prev *DoublyListNode // Pointer to the predecessor
}
// Initialize a node with NewDoublyListNode
func NewDoublyListNode(val int) *DoublyListNode {
return &DoublyListNode{
Val: val,
Next: nil,
Prev: nil,
}
}
/* Doubly linked list node class */
class ListNode {
val: number;
next: ListNode | null;
prev: ListNode | null;
constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {
this.val = val === undefined ? 0 : val; // Node value
this.next = next === undefined ? null : next; // Reference to the successor
this.prev = prev === undefined ? null : prev; // Reference to the predecessor
}
}
use std::rc::Rc;
use std::cell::RefCell;
/* Doubly linked list node type */
#[derive(Debug)]
struct ListNode {
val: i32, // Node value
next: Option<Rc<RefCell<ListNode>>>, // Pointer to the successor
prev: Option<Rc<RefCell<ListNode>>>, // Pointer to the predecessor
}
/* Constructor */
impl ListNode {
fn new(val: i32) -> Self {
ListNode {
val,
next: None,
prev: None,
}
}
}
/* Doubly linked list node structure */
typedef struct ListNode {
int val; // Node value
struct ListNode *next; // Pointer to the successor
struct ListNode *prev; // Pointer to the predecessor
} ListNode;
/* Constructor */
ListNode *newListNode(int val) {
ListNode *node;
node = (ListNode *) malloc(sizeof(ListNode));
node->val = val;
node->next = NULL;
node->prev = NULL;
return node;
}

Figure 4-8 Common types of linked lists
4.2.4 Typical applications of linked lists¶
Singly linked lists commonly implement stacks, queues, hash tables, and graphs.
- Stacks and queues: Inserting and removing at the same end gives last-in, first-out behavior, forming a stack. Inserting at one end and removing at the other gives first-in, first-out behavior, forming a queue.
- Hash tables: Separate chaining is a common collision-handling strategy that places colliding elements in a linked list.
- Graphs: An adjacency list associates each vertex with a linked list whose elements represent the other vertices connected to it.
Doubly linked lists are useful when both the previous and next elements must be found quickly.
- Advanced data structures: In structures such as red-black trees and B-trees, a node may need access to its parent. Storing a parent reference is similar to the backward link in a doubly linked list.
- Browser history: The forward and back buttons require access to the next and previous visited pages. A doubly linked list makes this straightforward.
- LRU caching: Least recently used eviction needs fast access to the least recently used data and efficient insertion and removal, making doubly linked lists a good fit.
Circular linked lists suit cyclic operations, such as operating-system resource scheduling.
- Round-robin scheduling: This common CPU scheduling algorithm cycles through processes, giving each a time slice before moving to the next. A circular linked list can implement this cycle.
- Data buffers: Some buffers use circular linked lists. Audio and video players, for example, may divide a stream into buffer blocks arranged in a circular list for seamless playback.