DSA Linked Lists
A linked list is a linear data structure where elements are stored in individual units called nodes. Unlike arrays, the nodes in a linked list are not stored in contiguous memory. Instead, each node holds two things: the data it stores and a pointer (reference) to the next node in the sequence.
This structure makes linked lists particularly efficient for insertions and deletions, especially when the position is already known.
How a Linked List Looks
Visualize a linked list like a chain of train cars. Each car (node) carries cargo (data) and is connected to the next car via a coupling (pointer). The first car is called the head, and the last car has no coupling — its pointer is None.
# [10] --> [20] --> [30] --> [40] --> None
Building a Linked List in Python
Python does not have a built-in linked list, so one must be created manually using classes.
Step 1: Define the Node Class
class Node:
def __init__(self, data):
self.data = data # The value stored in this node
self.next = None # Pointer to the next node (default: None)
Step 2: Define the LinkedList Class
class LinkedList:
def __init__(self):
self.head = None # Initially the list is empty
Core Linked List Operations
Inserting at the End — O(n)
To add a node at the end, traverse to the last node, then attach the new node there.
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
new_node = Node(data)
if self.head is None:
self.head = new_node
return
current = self.head
while current.next is not None:
current = current.next
current.next = new_node
# Usage
ll = LinkedList()
ll.append(10)
ll.append(20)
ll.append(30)
Inserting at the Beginning — O(1)
Adding at the front is faster because only the head pointer needs to change.
def prepend(self, data):
new_node = Node(data)
new_node.next = self.head
self.head = new_node
Traversing the List — O(n)
To display or process all elements, start from the head and follow the next pointers until reaching None.
def display(self):
current = self.head
while current is not None:
print(current.data, end=" --> ")
current = current.next
print("None")
# Output: 10 --> 20 --> 30 --> None
Deleting a Node — O(n)
To delete a node, find the node just before the target and update its next pointer to skip over the target node.
def delete(self, data):
if self.head is None:
return
# If the node to delete is the head
if self.head.data == data:
self.head = self.head.next
return
current = self.head
while current.next is not None:
if current.next.data == data:
current.next = current.next.next # Skip the target node
return
current = current.next
Searching for a Value — O(n)
def search(self, target):
current = self.head
position = 0
while current is not None:
if current.data == target:
return position
current = current.next
position += 1
return -1 # Not found
ll = LinkedList()
ll.append(5)
ll.append(15)
ll.append(25)
print(ll.search(15)) # Output: 1
Types of Linked Lists
1. Singly Linked List
Each node points only to the next node. Traversal is one-directional — from head to tail.
# [A] --> [B] --> [C] --> None
2. Doubly Linked List
Each node has two pointers: one pointing to the next node and one to the previous node. This allows traversal in both directions.
class DNode:
def __init__(self, data):
self.data = data
self.next = None
self.prev = None # Extra pointer to previous node
# None <-- [A] <--> [B] <--> [C] --> None
3. Circular Linked List
The last node's next pointer points back to the head instead of None, forming a loop. Useful in applications like round-robin scheduling.
# [A] --> [B] --> [C] --> back to [A]
Complete Working Example
class Node:
def __init__(self, data):
self.data = data
self.next = None
class LinkedList:
def __init__(self):
self.head = None
def append(self, data):
new_node = Node(data)
if not self.head:
self.head = new_node
return
current = self.head
while current.next:
current = current.next
current.next = new_node
def display(self):
current = self.head
while current:
print(current.data, end=" --> ")
current = current.next
print("None")
# Create list and add items
ll = LinkedList()
ll.append(100)
ll.append(200)
ll.append(300)
ll.display()
# Output: 100 --> 200 --> 300 --> None
Arrays vs Linked Lists
- Access: Arrays allow O(1) random access; linked lists require O(n) traversal.
- Insertion at front: Linked lists do it in O(1); arrays require O(n) shifting.
- Memory: Arrays use less memory (no pointers); linked lists use extra memory per node for pointers.
- Dynamic size: Linked lists naturally grow and shrink; arrays need resizing.
Big O Summary for Linked Lists
- Access by index — O(n)
- Search — O(n)
- Insert at beginning — O(1)
- Insert at end — O(n) for singly; O(1) with tail pointer
- Delete at beginning — O(1)
- Delete by value — O(n)
Key Points
- A linked list consists of nodes, each storing data and a pointer to the next node.
- The first node is called the head; the last node points to
None. - Insertion at the beginning is O(1), making linked lists ideal for frequent front insertions.
- Singly linked lists travel in one direction; doubly linked lists travel in both directions.
- Circular linked lists form a loop and are useful for cyclic processes.
- Linked lists use more memory than arrays but are more efficient for certain insert/delete operations.
