Understanding Doubly Linked Lists
Introduction
A doubly linked list is a type of data structure in which each node contains a reference to both the previous and the next node in the sequence. This makes it easy to traverse the list in both forward and backward directions. Doubly linked lists are commonly used in computer science for various purposes such as implementing data structures like stacks, queues, and associative arrays.
In this article, we will discuss the implementation of a doubly linked list in Python. We will cover the basic operations such as insertion, deletion, and traversal of the list.
Implementation
We will start by defining the Node class which will represent each node in the doubly linked list.
The Node class has three attributes: data, prev, and next. The data attribute stores the value of the node, while prev and next store references to the previous and next nodes in the list, respectively. If a node does not have a previous or next node, its corresponding attribute will be set to None.
class Node:
def __init__(self, data):
self.data = data
self.prev = None
self.next = None
Insertion
The insertion operation in a doubly linked list involves adding a new node at a specific position in the list. Let's look at the different scenarios for inserting a node.
Insertion at the Beginning
To insert a node at the beginning of the doubly linked list, we need to update the
next
andprev
pointers accordingly. Theprev
pointer of the new node should beNone
as it becomes the first node. Thenext
pointer of the new node should point to the current first node. Finally, we update theprev
pointer of the current first node to point back to the new node.def insert_at_beginning(self, data): new_node = Node(data) if self.is_empty(): self.head = new_node self.tail = new_node else: new_node.next = self.head self.head.prev = new_node self.head = new_node
Insertion at the End
To insert a node at the end of the doubly linked list, we need to update the
next
andprev
pointers accordingly. Thenext
pointer of the new node should beNone
as it becomes the last node. Theprev
pointer of the new node should point to the current last node. Finally, we update thenext
pointer of the current last node to point to the new node.def insert_at_end(self, data): new_node = Node(data) if self.is_empty(): self.head = new_node self.tail = new_node else: new_node.prev = self.tail self.tail.next = new_node self.tail = new_node
Insertion at a Specific Position
To insert a node at a specific position, we need to update the
next
andprev
pointers of the surrounding nodes. Suppose we want to insert the new node after a node calledcurrent_node
. We set thenext
pointer of the new node to thenext
pointer ofcurrent_node
. We update theprev
pointer of the new node to point tocurrent_node
. Finally, we update thenext
pointer ofcurrent_node
to point to the new node.def insert_after(self, current_node, data): new_node = Node(data) new_node.prev = current_node new_node.next = current_node.next if current_node.next is not None: current_node.next.prev = new_node else: self.tail = new_node current_node.next = new_node
Deletion
Delete at the Beginning
To delete the first node in the doubly linked list, we need to update the
next
andprev
pointers accordingly. We update thenext
pointer of the second node toNone
to indicate that it is now the first node. Finally, we update theprev
pointer of the second node toNone
as there is no previous node.def delete_at_beginning(self): if self.is_empty(): print("The list is empty.") elif self.head == self.tail: self.head = None self.tail = None else: self.head = self.head.next self.head.prev = None
Delete at the End
To delete the last node in the doubly linked list, we need to update the
next
andprev
pointers accordingly. We update theprev
pointer of the second-to-last node toNone
to indicate that it is now the last node. Finally, we update thenext
pointer of the second-to-last node toNone
as there is no next node.def delete_at_end(self): if self.is_empty(): print("The list is empty.") elif self.head == self.tail: self.head = None self.tail = None else: self.tail = self.tail.prev self.tail.next = None
Delete a Specific Node
To delete a specific node, we need to update the
next
andprev
pointers of the surrounding nodes. Suppose we want to delete a node calledcurrent_node
. We update thenext
pointer of the previous node to point to the node aftercurrent_node
. We update theprev
pointer of the next node to point to the node beforecurrent_node
.def delete_node(self, node): if self.is_empty(): print("The list is empty.") elif node == self.head: self.delete_at_beginning() elif node == self.tail: self.delete_at_end() else: node.prev.next = node.next node.next.prev = node.prev
Displaying the Doubly Linked List
To display the elements of a doubly linked list, we can traverse the list from the beginning to the end, printing the data of each node along the way.
def display(self): if self.is_empty(): print("The list is empty.") else: current = self.head while current: print(current.data) current = current.next
Checking if the Doubly Linked List is Empty
To check if a doubly linked list is empty, we can simply verify if the
head
pointer isNone
. If thehead
isNone
, it means there are no nodes in the list.def is_empty(self): return self.head is None
Time Complexities
Insertion:
- Insertion at the end: O(n) - In the worst case, when inserting at the end, we need to traverse the entire list.
Display:
- O(n) - As we need to traverse the entire list to display its contents, the time complexity is linear.
Deletion:
- O(n) - Similar to insertion, the deletion operation may require traversing the entire list in the worst case.
Insertion at the Beginning:
- O(1) - Inserting a node at the beginning only involves updating the pointers, which can be done in constant time.
Is Empty:
- O(1) - Checking if the list is empty involves a simple comparison operation, resulting in constant time complexity.
Real-Time Example: A Music Playlist
Let's consider the scenario of managing an online playlist. Each song in the playlist can be represented as a node in a doubly linked list. The doubly linked list allows efficient traversal, insertion, deletion, and reordering of songs.
# Creating a playlist using a doubly linked list
playlist = DoublyLinkedList()
# Adding songs to the playlist
playlist.insert("Song 1")
playlist.insert("Song 2")
playlist.insert("Song 3")
playlist.insert("Song 4")
playlist.insert("Song 5")
# Displaying the playlist
playlist.display() # Output: Song 1 Song 2 Song 3 Song 4 Song 5
# Deleting a song from the playlist
playlist.delete("Song 3")
# Inserting a new song at the beginning of the playlist
playlist.insert_at_beginning("New Song")
# Checking if the playlist is empty
print(playlist.is_empty()) # Output: False
Here, we make a playlist and add several songs to it. The playlist is then displayed, a song ("Song 3") is deleted, a new song ("New Song") is added to the beginning, and lastly, we determine whether the playlist is empty.
Conclusion
Doubly linked lists are a versatile data structure with efficient operations for the insertion, deletion, traversal, and reordering of elements. Their ability to navigate in both forward and backward directions makes them suitable for various applications. By understanding the implementation and operations of a doubly linked list, you can leverage this data structure in real-world scenarios like playlist management, browser history, and more.