How to Implement a Linked List in Python from Scratch
Implementing a linked list in Python requires creating a custom Node class to store data and a reference to the next node, and a LinkedList class to manage these nodes. By linking these objects together via pointers, you create a dynamic data structure that allows for efficient insertions and deletions compared to standard Python lists.
How to Implement a Linked List in Python from Scratch
A linked list is a linear collection of data elements called nodes, where each node points to the next. Unlike arrays or Python's built-in lists, linked lists do not store elements in contiguous memory locations, making them highly efficient for frequent insertions and deletions.
Understanding the Core Components
To build a singly linked list, you must define two distinct classes: the Node and the LinkedList.
1. The Node Class
The Node is the fundamental building block. It contains two attributes:
* Data: The actual value being stored (integer, string, etc.).
* Next: A reference (pointer) to the next node in the sequence. By default, this is set to None.
2. The LinkedList Class
The LinkedList class acts as the manager. Its primary responsibility is to keep track of the head (the first node in the list). If the head is None, the list is empty.
Step-by-Step Implementation
Below is the complete technical implementation of a singly linked list in Python.
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
"""Adds a new node to the end of the list."""
new_node = Node(data)
if not self.head:
self.head = new_node
return
last_node = self.head
while last_node.next:
last_node = last_node.next
last_node.next = new_node
def prepend(self, data):
"""Adds a new node to the beginning of the list."""
new_node = Node(data)
new_node.next = self.head
self.head = new_node
def delete(self, key):
"""Removes the first occurrence of a node with the specified data."""
temp = self.head
# If the head node itself holds the key
if temp is not None:
if temp.data == key:
self.head = temp.next
temp = None
return
# Search for the key to be deleted
while temp is not None:
if temp.next is not None:
if temp.next.data == key:
temp.next = temp.next.next
temp = None
return
temp = temp.next
def display(self):
"""Prints the linked list in a readable format."""
elements = []
current = self.head
while current:
elements.append(str(current.data))
current = current.next
print(" -> ".join(elements) + " -> None")
Analyzing Primary Operations
Insertion (Append and Prepend)
Appending a node requires traversing the entire list to find the current tail, resulting in a time complexity of O(n). Prepending, however, only requires updating the head pointer, which is an O(1) operation.
Deletion
Deleting a node involves finding the node immediately preceding the target and updating its next pointer to skip the target node. This effectively removes the node from the chain, allowing Python's garbage collector to reclaim the memory.
Traversal
To access a specific element or print the list, you must start at the head and follow the next pointers sequentially. This is why linked lists do not support random access (indexing) as efficiently as arrays.
Time and Space Complexity
The efficiency of a linked list is best understood through Big O notation. For those preparing for technical screenings, it is helpful to understand How to Explain Big O Notation Simply for Technical Interviews to articulate these trade-offs.
| Operation | Time Complexity | Note |
|---|---|---|
| Access | O(n) | Must traverse from the head. |
| Prepend | O(1) | Immediate update of the head pointer. |
| Append | O(n) | Requires traversal to the end (unless a tail pointer is kept). |
| Deletion | O(n) | Requires searching for the element first. |
| Space | O(n) | Each element requires additional memory for the pointer. |
When to Use Linked Lists Over Arrays
While Python's list type is versatile, a manual linked list implementation is superior in specific scenarios:
- Frequent Insertions/Deletions: If your application requires adding or removing elements from the start of the list frequently, a linked list avoids the O(n) cost of shifting elements found in arrays.
- Dynamic Memory Allocation: Linked lists grow and shrink organically without needing to reallocate large contiguous blocks of memory.
- Implementing Other Structures: Linked lists serve as the foundation for stacks, queues, and more complex graphs.
For a deeper dive into the logic of this implementation, see the full guide on How to Implement a Linked List in Python from Scratch available on CodeAmber.
Best Practices for Implementation
To ensure your data structures are production-ready, follow these engineering standards:
- Handle Edge Cases: Always check if the list is empty (
self.head is None) before attempting to delete or traverse. - Encapsulation: Keep the
Nodeclass simple and handle all logic within theLinkedListmanager class. - Memory Management: Ensure references are properly cleared during deletion to prevent memory leaks.
Following these standards aligns with the 5 Essential Best Practices for Clean Code, ensuring your technical solutions remain maintainable and scalable.
Key Takeaways
- Structure: A linked list consists of nodes containing data and a pointer to the next node.
- Efficiency: Prepending is O(1), while accessing or appending (without a tail pointer) is O(n).
- Flexibility: Linked lists are ideal for dynamic data sets where the size is unknown or changes frequently.
- Implementation: Requires two classes—one for the individual data unit (Node) and one for the list management (LinkedList).