Table of Contents
- What is a Linked List?
- How Linked Lists Work: Nodes and Pointers
- Types of Linked Lists
- Basic Operations on Linked Lists
- Implementation in Python
- Advantages and Disadvantages
- Use Cases: When to Use Linked Lists
- Common Interview Questions
- Conclusion
- References
What is a Linked List?
A linked list is a linear data structure where elements (nodes) are not stored in contiguous memory locations. Instead, each node contains:
- Data: The value stored in the node (e.g., integers, strings).
- Pointer/Reference: A link to the next node (or previous node, in some variants).
Unlike arrays, linked lists have no fixed size—they grow or shrink dynamically as elements are added or removed. The first node is called the head, and the last node (which points to null or None) is called the tail.
How Linked Lists Work: Nodes and Pointers
Let’s visualize a simple linked list. Suppose we have a list of integers: 10 → 20 → 30 → None.
- Nodes: Each element (10, 20, 30) is a node.
- Pointers: The node with data
10has a pointer to the node with20, which points to30, which points toNone(indicating the end).
[10 | next] → [20 | next] → [30 | next] → None
↑ ↑ ↑
Head Node 2 Tail
Without contiguous memory, nodes can be scattered across the system, but pointers ensure they’re linked in sequence.
Types of Linked Lists
Linked lists come in several flavors, each optimized for specific use cases.
1. Singly Linked List
The simplest type: each node has only a “next” pointer (no “previous” pointer). Traversal is unidirectional (only from head to tail).
Structure:
Node(data, next)
Head → Node1 → Node2 → ... → NodeN → None
Example: A to-do list where you only need to add/remove items from the start.
2. Doubly Linked List
Each node has two pointers: prev (to the previous node) and next (to the next node). Traversal is bidirectional (forward and backward).
Structure:
Node(data, prev, next)
None ← Node1 ← Node2 ← ... ← NodeN → None
Example: Browser history (back/forward buttons) or music playlists (skip to previous/next song).
3. Circular Linked List
The tail node points back to the head, forming a loop. No None pointer exists. Variants include:
- Circular Singly: Only
nextpointers (e.g., a round-robin task scheduler). - Circular Doubly: Both
prevandnextpointers (e.g., a clock application showing 12 → 1 → 2… → 12).
Structure (Circular Singly):
Head → Node1 → Node2 → ... → NodeN → Head
Basic Operations on Linked Lists
Let’s explore core operations for a singly linked list (extensions to other types are similar).
1. Traversal
Visit each node to access or print data. Start at the head and follow next pointers until None is reached.
Steps:
- Initialize a temporary pointer
currenttohead. - While
currentis notNone:- Print
current.data. - Move
currenttocurrent.next.
- Print
Time Complexity: O(n) (n = number of nodes).
2. Insertion
Add a new node at the beginning, end, or middle of the list.
a. Insert at Head (Beginning)
- Create a new node.
- Set the new node’s
nextto the currenthead. - Update
headto the new node.
Example: Insert 5 at the head of 10 → 20 → 30 → None:
5 → 10 → 20 → 30 → None
Time Complexity: O(1).
b. Insert at Tail (End)
- Create a new node.
- If the list is empty (
head = None), setheadto the new node. - Otherwise, traverse to the tail (last node with
next = None), then set the tail’snextto the new node.
Example: Insert 40 at the tail of 10 → 20 → 30 → None:
10 → 20 → 30 → 40 → None
Time Complexity: O(n) (traversal to tail).
c. Insert at Position
Insert a node at a specific index (e.g., position 2, 0-based).
Steps:
- If
position = 0, insert at head (O(1)). - Else, traverse to the node at
position-1(call itprev_node). - Set
new_node.next = prev_node.next. - Set
prev_node.next = new_node.
Example: Insert 25 at position 2 in 10 → 20 → 30 → None:
10 → 20 → 25 → 30 → None
Time Complexity: O(n) (traversal to position).
3. Deletion
Remove a node by value or position.
a. Delete by Value
- If the list is empty, return.
- If the head node has the value, update
headtohead.next(O(1)). - Else, traverse to find the
prev_node(node before the target), then setprev_node.next = target.next.
Example: Delete 20 from 10 → 20 → 30 → None:
10 → 30 → None
Time Complexity: O(n) (traversal to find the node).
b. Delete by Position
Similar to insertion at position:
- If
position = 0, delete head. - Else, traverse to
position-1, then setprev_node.next = prev_node.next.next.
Time Complexity: O(n).
4. Search
Check if a value exists in the list. Traverse nodes and compare data.
Steps:
- Start at
head, traverse each node. - If
current.data == target, returnTrue(or index). - If
Noneis reached, returnFalse.
Time Complexity: O(n).
Implementation in Python
Let’s code a singly linked list in Python. We’ll use classes for Node and LinkedList.
Step 1: Define the Node Class
Each node has data and a next pointer (initially None).
class Node:
def __init__(self, data):
self.data = data # Store data
self.next = None # Pointer to next node (initially None)
Step 2: Define the LinkedList Class
Includes methods for traversal, insertion, deletion, and search.
class LinkedList:
def __init__(self):
self.head = None # Initialize empty list (head is None)
# Traverse and print the list
def traverse(self):
current = self.head
while current:
print(current.data, end=" → ")
current = current.next
print("None") # Mark end of list
# Insert at head (beginning)
def insert_at_head(self, data):
new_node = Node(data)
new_node.next = self.head # New node points to current head
self.head = new_node # Update head to new node
# Insert at tail (end)
def insert_at_tail(self, data):
new_node = Node(data)
if not self.head: # If list is empty, new node is head
self.head = new_node
return
current = self.head
while current.next: # Traverse to last node
current = current.next
current.next = new_node # Last node points to new node
# Insert at a specific position (0-based index)
def insert_at_position(self, data, position):
if position < 0:
print("Position cannot be negative!")
return
new_node = Node(data)
if position == 0: # Insert at head
self.insert_at_head(data)
return
current = self.head
for _ in range(position - 1): # Traverse to position-1
if not current:
print("Position out of bounds!")
return
current = current.next
new_node.next = current.next # New node points to next node
current.next = new_node # Previous node points to new node
# Delete a node by value
def delete_by_value(self, value):
if not self.head: # Empty list
print("List is empty!")
return
# If head is the target
if self.head.data == value:
self.head = self.head.next
return
current = self.head
while current.next and current.next.data != value:
current = current.next
if not current.next: # Value not found
print(f"Value {value} not found!")
return
current.next = current.next.next # Skip the target node
# Search for a value
def search(self, value):
current = self.head
index = 0
while current:
if current.data == value:
return f"Value {value} found at index {index}"
current = current.next
index += 1
return f"Value {value} not found"
Step 3: Test the Implementation
Let’s create a linked list and test operations:
# Initialize list
ll = LinkedList()
# Insert elements
ll.insert_at_head(30) # List: 30 → None
ll.insert_at_head(20) # List: 20 → 30 → None
ll.insert_at_tail(40) # List: 20 → 30 → 40 → None
ll.insert_at_position(25, 2) # Insert 25 at index 2: 20 → 30 → 25 → 40 → None
# Traverse
print("Linked List after insertions:")
ll.traverse() # Output: 20 → 30 → 25 → 40 → None
# Search
print(ll.search(25)) # Output: Value 25 found at index 2
# Delete
ll.delete_by_value(30) # Delete 30: 20 → 25 → 40 → None
print("Linked List after deleting 30:")
ll.traverse() # Output: 20 → 25 → 40 → None
Advantages and Disadvantages
Advantages
- Dynamic Size: Grows/shrinks without pre-allocation (unlike arrays with fixed size).
- Efficient Insertions/Deletions: At the head (O(1)) or tail (O(1) with a tail pointer).
- No Memory Wastage: Only uses memory for existing nodes.
Disadvantages
- No Random Access: To access the nth element, you must traverse from the head (O(n) time; arrays have O(1) random access).
- Extra Memory Overhead: Pointers consume additional memory (e.g., 8 bytes per pointer on 64-bit systems).
- Complex Traversal: Singly linked lists can’t traverse backward; doubly linked lists fix this but add more pointers.
Use Cases: When to Use Linked Lists
Linked lists shine in scenarios requiring dynamic behavior or frequent insertions/deletions at the edges.
1. Browser History
Doubly linked lists store visited URLs, allowing O(1) access to previous/next pages (back/forward buttons).
2. Music Playlists
Add/remove songs from any position, or skip to previous/next tracks (doubly linked list).
3. Dynamic Memory Allocation
Operating systems use linked lists to track free memory blocks (e.g., malloc in C uses a linked list of free chunks).
4. Hash Table Chaining
To handle collisions, hash tables store conflicting entries in a linked list (e.g., Python’s dict uses this).
5. Implementing Stacks/Queues
Linked lists provide dynamic size for stacks (LIFO) and queues (FIFO), avoiding array overflow.
6. Graph Adjacency Lists
Graphs are often represented with linked lists, where each node points to its neighbors (efficient for sparse graphs).
Common Interview Questions
Linked lists are a staple of coding interviews. Here are key questions to practice:
- Reverse a Linked List: Iterative or recursive approach (swap pointers).
- Detect a Cycle: Use Floyd’s Tortoise and Hare algorithm (two pointers, one fast, one slow).
- Find the Middle Node: Slow pointer (1 step) and fast pointer (2 steps); when fast reaches end, slow is at middle.
- Merge Two Sorted Linked Lists: Combine into a single sorted list (similar to merge sort).
- Remove Duplicates: Traverse and skip duplicates (use a set or compare adjacent nodes).
Conclusion
Linked lists are a foundational data structure, offering flexibility for dynamic data. While they lack arrays’ random access, their efficiency in insertions/deletions and dynamic sizing makes them indispensable in applications like browser history, playlists, and memory management.
By mastering linked lists, you’ll gain intuition for pointers, recursion, and algorithm design—skills that transfer to more complex structures like trees and graphs. Practice implementing operations and solving interview problems to solidify your understanding!
References
- GeeksforGeeks. “Linked List Data Structure.” https://www.geeksforgeeks.org/data-structures/linked-list/
- Wikipedia. “Linked List.” https://en.wikipedia.org/wiki/Linked_list
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.