Of course! Here is a comprehensive guide to implementing and using a Linked List in Python.
What is a Linked List?
A Linked List is a linear data structure, but unlike an array, its elements are not stored at contiguous memory locations. Instead, each element is a separate object called a node.
Each node contains two pieces of information:
- Data: The actual value the node holds.
- Next: A pointer (or reference) to the next node in the sequence. The last node's
nextpointer points toNone.
The entire list is accessed through a single pointer, usually called the head, which points to the first node.
Analogy: Think of a treasure hunt. Each clue (node) tells you where to find the next clue (the next pointer). The head is the first clue you are given.
The Node Class
First, we need to create the building block of a linked list: the Node.
class Node:
"""A node in a singly linked list."""
def __init__(self, data):
self.data = data # The data stored in the node
self.next = None # The pointer to the next node, initially None
def __repr__(self):
# A helpful representation for printing the node
return f"Node({self.data})"
The LinkedList Class
Now, let's create the LinkedList class itself. This class will manage the nodes and provide methods to interact with the list.
Basic Structure and Initialization
class LinkedList:
"""A singly linked list."""
def __init__(self):
self.head = None # The head of the list, initially empty
def __repr__(self):
"""Returns a string representation of the list."""
nodes = []
current_node = self.head
while current_node:
nodes.append(str(current_node.data))
current_node = current_node.next
return " -> ".join(nodes) + " -> None"
def is_empty(self):
"""Returns True if the list is empty, False otherwise."""
return self.head is None
Core Operations
Here are the fundamental methods for manipulating a linked list.
a. Append (Add to the End)
This is the most common operation. We traverse the list to find the last node and then link the new node to it.
def append(self, data):
"""Adds a new node with the given data to the end of the list."""
new_node = Node(data)
# If the list is empty, the new node becomes the head
if self.is_empty():
self.head = new_node
return
# Otherwise, traverse to the end of the list
last_node = self.head
while last_node.next:
last_node = last_node.next
# Link the last node to the new node
last_node.next = new_node
b. Prepend (Add to the Beginning)
This is very efficient as it doesn't require traversal. We just update the head.
def prepend(self, data):
"""Adds a new node with the given data to the beginning of the list."""
new_node = Node(data)
new_node.next = self.head # The new node now points to the old head
self.head = new_node # The new node becomes the new head
c. Delete (by Value)
This involves finding the node with the given data and removing it from the chain. We need to keep track of the previous node to "stitch" the list back together.
def delete(self, data):
"""
Deletes the first occurrence of a node with the given data.
"""
# Case 1: The list is empty
if self.is_empty():
print("List is empty, cannot delete.")
return
# Case 2: The node to delete is the head
if self.head.data == data:
self.head = self.head.next
return
# Case 3: The node is somewhere else in the list
# We need to find the node *before* the one to delete
current_node = self.head
while current_node.next and current_node.next.data != data:
current_node = current_node.next
# If we reached the end, the data was not found
if current_node.next is None:
print(f"Data '{data}' not found in the list.")
else:
# Unlink the node by skipping over it
current_node.next = current_node.next.next
Putting It All Together: A Full Example
Here is the complete code for the Node and LinkedList classes with all the methods discussed.
class Node:
"""A node in a singly linked list."""
def __init__(self, data):
self.data = data
self.next = None
def __repr__(self):
return f"Node({self.data})"
class LinkedList:
"""A singly linked list."""
def __init__(self):
self.head = None
def __repr__(self):
"""Returns a string representation of the list."""
nodes = []
current_node = self.head
while current_node:
nodes.append(str(current_node.data))
current_node = current_node.next
return " -> ".join(nodes) + " -> None"
def is_empty(self):
"""Returns True if the list is empty, False otherwise."""
return self.head is None
def append(self, data):
"""Adds a new node with the given data to the end of the list."""
new_node = Node(data)
if self.is_empty():
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 with the given data to the beginning of the list."""
new_node = Node(data)
new_node.next = self.head
self.head = new_node
def delete(self, data):
"""Deletes the first occurrence of a node with the given data."""
if self.is_empty():
print("List is empty, cannot delete.")
return
if self.head.data == data:
self.head = self.head.next
return
current_node = self.head
while current_node.next and current_node.next.data != data:
current_node = current_node.next
if current_node.next is None:
print(f"Data '{data}' not found in the list.")
else:
current_node.next = current_node.next.next
def find(self, data):
"""Searches for a node with the given data and returns it, or None."""
current_node = self.head
while current_node:
if current_node.data == data:
return current_node
current_node = current_node.next
return None
# --- DEMONSTRATION ---
# 1. Create a new linked list
ll = LinkedList()
print(f"Initial list: {ll}")
print(f"Is list empty? {ll.is_empty()}")
print("-" * 20)
# 2. Append some nodes
ll.append(10)
ll.append(20)
ll.append(30)
print(f"After appending 10, 20, 30: {ll}")
print(f"Is list empty? {ll.is_empty()}")
print("-" * 20)
# 3. Prepend a node
ll.prepend(5)
print(f"After prepending 5: {ll}")
print("-" * 20)
# 4. Delete a node from the middle
ll.delete(20)
print(f"After deleting 20: {ll}")
print("-" * 20)
# 5. Delete the head node
ll.delete(5)
print(f"After deleting head (5): {ll}")
print("-" * 20)
# 6. Delete the tail node
ll.delete(30)
print(f"After deleting tail (30): {ll}")
print("-" * 20)
# 7. Try to delete a node that doesn't exist
ll.delete(99)
print(f"After trying to delete 99: {ll}")
print("-" * 20)
# 8. Find a node
found_node = ll.find(10)
if found_node:
print(f"Found node: {found_node}")
else:
print("Node not found.")
Linked List vs. Python list
It's important to understand when to use a linked list versus Python's built-in list.
| Feature | Linked List | Python list (Dynamic Array) |
|---|---|---|
| Memory Usage | Higher. Each node stores data and a pointer. | Lower. Stores data in a contiguous block of memory. |
| Insertion/Deletion (Beginning) | O(1). Just change the head pointer. |
O(n). All other elements must be shifted. |
| Insertion/Deletion (End) | O(n) (unless you maintain a tail pointer). |
O(1) (Amortized). |
| Insertion/Deletion (Middle) | O(n). Must find the location first. | O(n). Must find the location and shift elements. |
Accessing Element (Index i) |
O(n). Must traverse from the head. | O(1). Direct memory access via index. |
| Cache Performance | Poor. Data is scattered in memory. | Excellent. Data is contiguous, so it's "cache-friendly". |
Conclusion: You would almost always prefer Python's built-in list for its speed and simplicity. A linked list is primarily an academic data structure used to teach pointer manipulation and is the foundation for more complex structures like Stacks, Queues, and Graphs.
