A Doubly Linked List (DLL) is a linear data structure where elements are stored in nodes, and each node contains pointers to both the next and previous nodes. Unlike Singly Linked Lists, DLL allows bidirectional traversal. DLLs are important for coding interviews, memory management, and implementing structures like deque and LRU cache.
Structure of a Doubly Linked List
A doubly linked list consists of:
Head → Reference to the first node
Tail → Reference to the last node (optional)
Nodes → Each node contains:
- Data
- Pointer to the next node (
next_element) - Pointer to the previous node (
previous_element)
None <- [Prev | Data | Next] <-> [Prev | Data | Next] <-> [Prev | Data | Next] -> None
Key Characteristics
- Each node points both forward and backward
- Traversal can start from head or tail
- Last node points next to None, first node points previous to None
Core Components of DLL
To implement a DLL, we need two classes:
- Node
- DoublyLinkedList
1. Node Class
Components
- Data → Value stored in the node
- Pointer (next_element) → Reference to the next node
- Pointer (previous_element) → Reference to the previous node
Node Implementation
class Node:
def __init__(self, data):
# Store the value in the node
self.data = data
# Pointer to the next node
self.next_element = None
# Pointer to the previous node
self.previous_element = None
Explanation
dataholds the valuenext_elementlinks to the next nodeprevious_elementlinks to the previous node- Initially, both pointers are
None
2. DoublyLinkedList Class
Head and Tail
- head_node → Reference to the first node
- tail_node → Reference to the last node
Initialization
class DoublyLinkedList:
def __init__(self):
# Start with an empty list
self.head_node = None
self.tail_node = None
Helper Functions
get_head()
def get_head_node(self):
# Return the head node of the list
return self.head_node
Time Complexity: O(1)
is_empty()
def is_empty(self):
# Returns True if list is empty
return self.head_node is None
Time Complexity: O(1)
Insertion Operations
Insert at Head
Concept:
- New node becomes the first node
- New node points to the old head
- Old head’s previous points to new node
- Update tail if list was empty
def insert_at_head(self, data):
# Create a new node
node = Node(data)
# Link new node to current head
node.next_element = self.head_node
# If list is empty, set both head and tail to new node
if self.is_empty():
self.tail_node = node
self.head_node = node
# If list is not empty, update previous of old head and move head
else:
self.head_node.previous_element = node
self.head_node = node
Time Complexity: O(1)
Insert at Tail
Concept:
- Attach new node at the end
- Update previous pointer of new node
- Update tail
def insert_at_tail(self, data):
# Create a new node
node = Node(data)
# If list is empty, set both head and tail to new node
if self.is_empty():
self.head_node = node
self.tail_node = node
return
# If list is not empty, link node after tail and move tail
else:
self.tail_node.next_element = node
node.previous_element = self.tail_node
self.tail_node = node
Time Complexity: O(1)
Insert at k-th Position
Concept:
- Traverse to (k-1) node
- Rewire pointers for next and previous
def insert_at_k_node(self, data, k):
# Create a new node
node = Node(data)
# If position is 1, insert at head
if k == 1:
self.insert_at_head(data)
return
# Traverse to (k-1)th node
temp = self.head_node
position = 1
while position < k-1:
temp = temp.next_element
position += 1
# If insertion is at the end, insert at tail
if temp.next_element is None:
self.insert_at_tail(data)
return
# Insert node between (k-1)th and kth nodes
else:
node.next_element = temp.next_element
node.previous_element = temp
temp.next_element.previous_element = node
temp.next_element = node
Time Complexity: O(n)
Deletion Operations
Delete at Head
Concept:
- Move head to next node
- Update previous pointer of new head
- Update tail if list becomes empty
def delete_head(self):
# If list is empty, nothing to delete
if self.is_empty():
return
# Move head to next node
self.head_node = self.head_node.next_element
# If list becomes empty, update tail
if self.head_node is None:
self.tail_node = None
# Otherwise, update previous pointer of new head
else:
self.head_node.previous_element = None
Time Complexity: O(1)
Delete at Tail
Concept:
- Update second last node’s next to None
- Update tail pointer
def delete_tail(self):
# If list is empty, nothing to delete
if self.is_empty():
return
# If list has only one node, clear head and tail
if self.head_node.next_element is None:
self.head_node = None
self.tail_node = None
return
# Move tail to previous node and update next pointer
else:
temp = self.tail_node
self.tail_node = temp.previous_element
self.tail_node.next_ele
Time Complexity: O(1)
Delete k-th Node
Concept:
- Traverse to k-th node
- Update previous and next pointers to remove it
- Update head or tail if needed
def delete_k_node(self, k):
if self.is_empty():
return
if k == 1:
self.delete_head()
return
else:
temp = self.head_node
position = 1
while position < k:
temp = temp.next_element
position += 1
# If deleting the tail node
if temp.next_element is None:
self.delete_tail()
return
# Rewire pointers to remove k-th node
temp.previous_element.next_element = temp.next_element
temp.next_element.previous_element = temp.previous_element
Time Complexity: O(n)
Traversal and Printing
Concept:
- Start from head and move forward
def print_list(self):
temp = self.head_node
while temp is not None:
print(f"{temp.data} <->", end=" ")
temp = temp.next_element
print("NULL")
Time Complexity: O(n)
Search Operation
Concept:
- Traverse forward
- Return True if node contains key
def search(self, key):
temp = self.head_node
while temp is not None:
if temp.data == key:
return True
temp = temp.next_element
return False
Time Complexity: O(n)