Python DSA Series 03: Understanding Linked Lists
Python DSA Series 03: Understanding Linked Lists
In our previous posts, we saw that Python lists are fast for appends and access but slow for insertions and deletions at the beginning (O(n)). Stacks and Queues are abstract types that enforce rules to guarantee O(1) performance for specific use cases.
Now, we explore a fundamental data structure that tackles the insertion/deletion problem from a different angle: the Linked List.
What is a Linked List?
Unlike a Python list (dynamic array), which stores elements in a contiguous block of memory, a linked list stores elements in individual objects called nodes. Each node contains two pieces of information:
- The data (the value it holds).
- A pointer (or reference) to the next node in the sequence.
The linked list itself is defined by a single pointer to its first node, called the head. The last node in the list points to None, indicating the end of the list.
Visual Representation:
[Head] -> [Node(Data|Next)] -> [Node(Data|Next)] -> [Node(Data|Next)] -> None
This structure is the key to its performance. To insert or delete a node, you don’t need to shift other elements; you just need to change a few pointers.
Implementing a Linked List in Python
Python doesn’t have a built-in linked list, so we build it from scratch. This is a very common task in technical interviews.
The Node Class
First, we need a simple class to represent a node.
1
2
3
4
class Node:
def __init__(self, data):
self.data = data
self.next = None # Pointer to the next node
The SinglyLinkedList Class
Now, we create the main class that manages the nodes.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class SinglyLinkedList:
def __init__(self):
self.head = None # The list is initially empty
# Method to print the list's contents
def print_list(self):
current_node = self.head
while current_node:
print(current_node.data, end=" -> ")
current_node = current_node.next
print("None")
# Add a new node to the beginning of the list (O(1))
def prepend(self, data):
new_node = Node(data)
new_node.next = self.head
self.head = new_node
# Add a new node to the end of the list (O(n))
def append(self, data):
new_node = Node(data)
if not self.head: # If the list is empty
self.head = new_node
return
last_node = self.head
while last_node.next: # Traverse to the last node
last_node = last_node.next
last_node.next = new_node
# Delete a node with a given key (O(n))
def delete_node(self, key):
current_node = self.head
# If the head node itself holds the key
if current_node and current_node.data == key:
self.head = current_node.next
current_node = None
return
# Search for the key to be deleted
prev = None
while current_node and current_node.data != key:
prev = current_node
current_node = current_node.next
# If the key was not present in the list
if not current_node:
return
# Unlink the node from the list
prev.next = current_node.next
current_node = None
Example Usage
1
2
3
4
5
6
7
8
9
10
llist = SinglyLinkedList()
llist.append("A")
llist.append("B")
llist.prepend("Start")
llist.append("C")
llist.print_list() # Output: Start -> A -> B -> C -> None
llist.delete_node("B")
llist.print_list() # Output: Start -> A -> C -> None
Time Complexity: Linked List vs. Python List
Here’s how linked lists stack up against Python’s dynamic arrays (list).
| Operation | Python list |
Singly Linked List | Why the Difference? |
|---|---|---|---|
| Access (by index) | O(1) | O(n) | Python lists have contiguous memory, allowing instant index calculation. Linked lists must be traversed from the head. |
| Search (by value) | O(n) | O(n) | Both may need to scan the entire structure. |
| Insertion (at beginning) | O(n) | O(1) | A Python list must shift all elements. A linked list only needs to update the head pointer. |
| Deletion (at beginning) | O(n) | O(1) | Same reason as insertion. |
| Insertion (at end) | O(1) | O(n) | A Python list has amortized O(1) appends. A singly linked list must traverse to the end to append. (This can be O(1) if we also store a tail pointer). |
| Deletion (at end) | O(1) | O(n) | A Python list can pop in O(1). A singly linked list must traverse to the second-to-last node. |
Types of Linked Lists
- Singly Linked List: The one we just built. Nodes have a single pointer to the next node. Traversal is one-way.
- Doubly Linked List: Each node has two pointers: one to the
nextnode and one to thepreviousnode. This allows for backward traversal and makes deleting a specific node O(1) if you already have a reference to it. The trade-off is higher memory usage. - Circular Linked List: The
nextpointer of the last node points back to theheadinstead ofNone. This can be useful for applications where you need to cycle through a list of items continuously (e.g., a slideshow).
Conclusion
Linked lists are a foundational concept in computer science, and building one is a rite of passage in coding interviews.
Key Trade-offs:
- Choose a Linked List when you need fast insertions and deletions at the beginning of your sequence and you don’t need fast random access to elements by index.
- Stick with a Python
listfor almost everything else. Its O(1) access and append operations, combined with better cache performance (due to contiguous memory), make it the superior general-purpose choice.
Understanding linked lists is less about using them in day-to-day Python and more about understanding the fundamental trade-offs between different data storage patterns.
Next up, we’ll explore Hash Tables (the powerhouse behind Python’s dictionaries), a data structure that provides average O(1) performance for lookups, insertions, and deletions.
Suggested Reading
- Linked Lists in Python (Real Python)
- Data Structures & Algorithms in Python - A comprehensive textbook.