Exploring Time Complexity of Doubly Linked List Operations
This article is a complementary resource to the DSA with Python and DSA with C++ courses.
This article is a complementary resource to the DSA with Python and DSA with C++ courses.
In this article, we'll explore the time complexity of common doubly linked list operations (such as traversal, insertion, deletion, and search) to better understand their performance characteristics.
Traversing a linked list involves visiting each element in a sequence.
def traverse(self):
current = self.head
print("None", end = " <-> ")
while current is not None:
print(f"{current.data} <-> ", end="")
current = current.next
print("None")
void traverse() {
// Start traversal from head
Node<T>* current = head;
cout << "nullptr <-> ";
while (current != nullptr) {
cout << current->data << " <-> ";
// Move to the next node
current = current->next;
}
cout << "nullptr\n";
}
The total time required to traverse the list is proportional to the number of nodes. If the list contains n nodes, it results in a time complexity of O(n).
However, for an empty linked list (i.e., when the head is nullptr or None), the traversal loop does not execute, and the time complexity is O(1).
Insertion involves adding a new node to the linked list. The time complexity of insertion depends on the location of the insertion point.
Here is how we can insert a node at the beginning of the linked list:
def insert_at_beginning(self, data):
# create new node
new_node = Node(data)
# update pointers
new_node.next = self.head
self.head.prev = new_node
# update head
self.head = new_node
void insert_at_beginning(T data){
// Create a new node to append
Node<T>* new_node = new Node<T>({data});
// If the list is empty
// new_node is both
// the head and tail
if (head == nullptr){
head = new_node;
tail = new_node;
return;
}
// head will be next to new_node
new_node->next = head;
// head is prev to new_node
head->prev = new_node;
// new_node is the head
head = new_node;
}
Inserting at the beginning of the linked list only requires an update in the head pointer, which is a constant time operation. So, this results in the time complexity of O(1) .
Here is how you can add a node to the end of a doubly linked list:
def append_node(self, data):
# initialize a new node
new_node = Node(data)
# update links
new_node.prev = self.tail
self.tail.next = new_node
# update tail
self.tail = new_node
void append(T data){
// Create a new node to append
Node<T>* new_node = new Node<T>({data});
// If the list is empty
// new_node is both the
// head and tail
if (head == nullptr){
head = new_node;
tail = new_node;
return;
}
// prev of new_node is the current tail
new_node->prev = tail;
// new_node is next to current tail
tail->next = new_node;
// new_node is the tail now
tail = new_node;
}
Inserting at the end of the doubly linked list only requires an update in the tail pointer, which is a constant time operation. So, this results in the time complexity of O(1).
Here's how you can insert a node at a position other than beginning or the end of a doubly linked list:
def insert_at_position(self, position, data):
# create new node
new_node = Node(data)
# bring a pointer to desired position
current = self.head
for i in range(position - 1):
current = current.next
# get a pointer to the next node
next_node = current.next
# update pointers
current.next = new_node
next_node.prev = new_node
new_node.prev = current
new_node.next = next_node
void insert_at_position(T data, int position){
int i = 1;
// Start from the head
Node<T>* current = head;
while (i < position - 1){
// Move to next node
current = current -> next;
++i;
}
// Get a pointer to next node
Node<T>* next_node = current->next;
// new_node is next to current_node.
current->next = new_node;
// new_node is prev to next_node
next_node->prev = new_node;
// prev of new_node is current
new_node->prev = current;
// next of new_node is
// next of current node
new_node->next = next_node;
}
Inserting nodes at positions other than the beginning of the list requires:
O(n).O(1). As the total computing cost of this operation is dominated by the traversal, the time complexity of this entire operation is O(n).
Here is how you can insert a node into an empty list:
def append_to_empty(self, data):
new_node = Node(data)
self.head = new_node
self.tail = new_node
void insert_into_empty(T data) {
// Create a new node to append
Node<T>* new_node = new Node<T>({data});
if (head == nullptr){
head = new_node;
tail = new_node;
return;
}
}
Inserting into an empty linked list only requires an update in the head pointer, which is a constant time operation. So, this results in a time complexity of O(1).
Deletion involves removing a node from the linked list. The time complexity of deletion depends on the location of the node to be deleted.
Here is how you can delete the first node from a doubly linked list:
def delete_from_beginning(self):
current = self.head
self.head = self.head.next
self.head.prev = None
# Update the next of current
current.next = None
void delete_from_beginning(){
if(head == nullptr) return;
// head is to be deleted
Node<T>* node_to_delete = head;
// next of head is the new head
head = head->next;
// prev of head is nullptr
head->prev = nullptr;
// Deallocate the memory
delete node_to_delete;
}
Deleting the first node of a linked list only requires an update in the head pointer, which is a constant time operation. So, this results in the time complexity of O(1).
Here's how you can delete the last node of a doubly linked list:
def delete_from_end(self):
# Get reference to the tail node
current = self.tail
self.tail = self.tail.prev
self.tail.next = None
current.prev = None
void delete_from_end(){
// tail is the end node
Node<T>* node_to_delete = tail;
// New tail is the previous
// of the previous tail
tail = tail->prev;
tail->next = nullptr;
node_to_delete->prev = nullptr;
// Deallocate the memory
delete node_to_delete;
}
Deleting the last node of a linked list only requires an update in the tail pointer, which is a constant time operation. So, this results in the time complexity of O(1).
Here is how you can delete a node of a doubly linked list in some position other than the beginning or the end of the list.
def delete_from_position(self, position):
current = self.head
prev_node = next_node = None
for i in range(position-1):
current = current.next
prev_node = current.prev
next_node = current.next
prev_node.next = next_node
next_node.prev = prev_node
current.next = None
current.prev = None
void delete_from_position(int position){
int i = 1;
Node<T>* current = head;
// Loop until we reach the position to delete
while (i < position){
// Move to next node
current = current->next;
++i;
}
// Get the pointer to prev and next of current node
Node<T>* prev_node = current->prev;
Node<T>* next_node = current->next;
// Update pointers
prev_node->next = next_node;
next_node->prev = prev_node;
// Deallocate the memory
delete current;
}
Deleting nodes at positions other than the beginning or the end requires
O(n). O(1).As the total computing cost of this operation is dominated by the traversal, the time complexity of this entire operation is O(n).
| Operation | Time Complexity |
|---|---|
| Traversal | O(n) |
| Insertion at the Beginning | O(1) |
| Insertion at the End | O(1) |
| Insertion Into Other Nodes | O(n) |
| Insertion Into Empty Linked List | O(1) |
| Deletion at the Beginning | O(1) |
| Deletion at the End | O(1) |
| Deletion at Other Positions | O(n) |