A Comprehensive Guide to Understanding Linked Lists
Today, we will explore the fundamentals of linked lists. A linked list is a fundamental data structure used for storing a collection of objects in a linear sequence. Each object, or node, in a linked list contains two main components: the data it holds and a pointer (or reference) to the next object in the list. Linked lists are conceptually similar to arrays, but with a key difference: the ordering of elements is determined by these pointers, not by their physical placement in memory or array indices.
Key Use Cases for Linked Lists
There are several important use cases for linked lists. They are frequently employed to implement other essential data structures, such as: - Stacks - Queues - Hash tables
Furthermore, they play a crucial role in dynamic memory allocation.
Anatomy of a Linked List
Let's look at a basic example of a linked list. As mentioned, each object consists of both data and a next
pointer. A linked list maintains a pointer to the very first node, which is called the head
. It is also a common practice to keep a tail
pointer that references the last object in the list, allowing for efficient access to both ends.
The structure described so far is known as a singly linked list, as the pointers travel in only one direction.
Variations of Linked Lists
A more versatile version is the doubly linked list. In this variation, each object contains not only a next
pointer but also a previous
pointer that points to the preceding object in the chain. This allows for traversal in both forward and reverse directions.
Another variation is the circular linked list. In this implementation, the previous
pointer of the head
object points to the tail
, and the next
pointer of the tail
object points back to the head
, forming a complete circle.
Common Linked List Operations
Let's review some of the most common operations performed on linked lists. In this article, we will cover three fundamental actions: search, insert, and delete. The code examples provided are based on a working Python implementation.
First, let's define our node object. We need to keep track of over two pieces of information for each node:
- data
: The value stored in the node.
- next
: A pointer to the next node in the list.
- previous
: A pointer to the previous node (for doubly linked lists).
class Node:
def __init__(self, data):
self.data = data
self.next = None
self.previous = None
Initially, the list is empty, so the head
pointer points to null
.
1. Search Operation
The first operation we will discuss is the search function. Let's imagine we have the following list: 1 <-> 2 <-> 3
, and we want to find the node with a data value of 3
.
To do this, we start by setting a current
node variable to the head
of the linked list. We then iterate through the list as long as current
is not null
. In each iteration, we compare the data
of the current
node to our target value. If they match, we return the current
node. Otherwise, we move to the next node by setting current
equal to current.next
.
def search(self, target):
current = self.head
while current is not None:
if current.data == target:
return current
current = current.next
return None # Target not found
In our example, the search would pass over the nodes containing 1
and 2
before reaching the third node, where the data 3
matches our target.
Note: In the worst-case scenario, we would need to visit every node in the list, making the time complexity for a search operation O(n).
2. Insertion Operation
Next, let's discuss how to insert a new node at the front of our list. Suppose we want to insert a new object with a data value of 1
.
Here is the step-by-step process:
1. Set the new node's next
pointer to the current head
object.
2. Set the current head
object's previous
pointer to the new node.
3. Update the head
pointer to reference the newly added node.
4. Finally, set the new node's previous
pointer to null
, as it is now the first item.
With these steps, our new node is successfully added to the front of the list. This is a constant time operation, meaning it has a runtime complexity of O(1).
3. Deletion Operation
Finally, let's cover how to delete a node from a linked list. For this example, we will delete the node with a data value of 2
from the list 1 <-> 2 <-> 3
.
The node to be deleted (2
) has a previous
pointer to node 1
and a next
pointer to node 3
. The process involves two key steps:
1. We update the next
pointer of node 1
to point to node 3
(which is the next
node of 2
).
2. We then update the previous
pointer of node 3
to point to node 1
(which is the previous
node of 2
).
Similar to insertion, this is a constant time operation with a runtime of O(1).
Important Consideration: This O(1) complexity assumes you already have a pointer to the node you want to delete. If you only have the key (e.g., the value 2
) and not a direct reference to the node, you must first search the list to find it. As we discussed earlier, that search operation is O(n), making the overall deletion time O(n) in that case.
Join the 10xdev Community
Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.