- Published on
๐๐ Doubly Linked Lists: A Short Overview of how they work in Python ๐
- Authors
- Name
- Ketan Shukla
๐ Introduction to Doubly Linked Lists
Imagine a two-way street where you can travel forward and backward with equal ease. In the realm of data structures, a doubly linked list is exactly thatโa dynamic, flexible chain of nodes that can be traversed in both directions.
๐๏ธ ๐ผ๏ธ Visual Structure of a Doubly Linked List
graph LR
N1["Node 1\nPrev: None\nData: Apple\nNext: Node 2"] -->|Next| N2["Node 2\nPrev: Node 1\nData: Banana\nNext: Node 3"]
N2 -->|Prev| N1
N2 -->|Next| N3["Node 3\nPrev: Node 2\nData: Cherry\nNext: None"]
N3 -->|Prev| N2
style N1 fill:#FFD700,stroke:#333,stroke-width:2px
style N2 fill:#87CEFA,stroke:#333,stroke-width:2px
style N3 fill:#98FB98,stroke:#333,stroke-width:2px
๐ ๐งฉ Key Characteristics of Doubly Linked Lists
Doubly linked lists extend the capabilities of singly linked lists with unique features:
- Bidirectional traversal (forward and backward)
- Each node contains data, a next reference, and a previous reference
- More flexible manipulation of elements
- Slightly more memory overhead compared to singly linked lists
๐ ๐ Doubly vs Singly Linked Lists
graph TB
subgraph "Singly Linked List"
direction LR
S1["Node 1\nData: Apple\nNext: Node 2"] --> S2["Node 2\nData: Banana\nNext: Node 3"]
end
subgraph "Doubly Linked List"
direction LR
D1["Node 1\nPrev: None\nData: Apple\nNext: Node 2"] <--> D2["Node 2\nPrev: Node 1\nData: Banana\nNext: Node 3"]
end
style S1 fill:#FFD700,stroke:#333
style S2 fill:#87CEFA,stroke:#333
style D1 fill:#FFD700,stroke:#333,stroke-width:2px
style D2 fill:#87CEFA,stroke:#333,stroke-width:2px
๐ป Python Implementation of Doubly Linked List
class Node:
def __init__(self, data):
self.prev = None # Reference to previous node
self.data = data # The data value
self.next = None # Reference to next node
class DoublyLinkedList:
def __init__(self):
self.head = None # First node in the list
self.tail = None # Last node in the list
def append(self, data):
new_node = Node(data)
# If list is empty
if not self.head:
self.head = new_node
self.tail = new_node
return
# Connect new node to the end
new_node.prev = self.tail
self.tail.next = new_node
self.tail = new_node
def prepend(self, data):
new_node = Node(data)
# If list is empty
if not self.head:
self.head = new_node
self.tail = new_node
return
# Connect new node to the beginning
new_node.next = self.head
self.head.prev = new_node
self.head = new_node
def display_forward(self):
elements = []
current = self.head
while current:
elements.append(current.data)
current = current.next
return ' <-> '.join(map(str, elements))
def display_backward(self):
elements = []
current = self.tail
while current:
elements.append(current.data)
current = current.prev
return ' <-> '.join(map(str, elements))
# Example usage
my_list = DoublyLinkedList()
my_list.append("Apple")
my_list.append("Banana")
my_list.prepend("Cherry")
print("Forward:", my_list.display_forward())
print("Backward:", my_list.display_backward())
๐ฏ Common Operations and Time Complexities
๐ Advanced Features
๐ Inserting a Node at a Specific Position
def insert_after(self, target_data, new_data):
current = self.head
while current:
if current.data == target_data:
new_node = Node(new_data)
new_node.prev = current
new_node.next = current.next
if current.next:
current.next.prev = new_node
current.next = new_node
# Update tail if inserting at the last node
if current == self.tail:
self.tail = new_node
return True
current = current.next
return False
๐ Reversing a Doubly Linked List
def reverse(self):
current = self.head
while current:
# Swap next and prev pointers
current.next, current.prev = current.prev, current.next
# Move to next node
if not current.prev:
self.tail = self.head
self.head = current
break
current = current.prev
๐จ Real-world Applications
Browser History
Forward and backward navigation
Efficiently move between pages
Music Player
Next and previous track functionality
Easy playlist traversal
Undo/Redo Mechanisms
Bidirectional state tracking
Complex application histories
Memory Management
Dynamic memory allocation
Efficient block management
๐ Best Practices
Always update both next and prev pointers
Handle edge cases (empty list, single node)
Maintain head and tail references
Be careful with pointer manipulations
Consider using sentinel nodes for complex operations
๐ฎ ๐ฎ Interactive Example: Music Playlist
class MusicPlaylist:
def __init__(self):
self.current_song = None
def play_next(self):
if self.current_song and self.current_song.next:
self.current_song = self.current_song.next
return self.current_song.data
return None
def play_previous(self):
if self.current_song and self.current_song.prev:
self.current_song = self.current_song.prev
return self.current_song.data
return None
# Create a playlist
playlist = DoublyLinkedList()
playlist.append("Bohemian Rhapsody")
playlist.append("Stairway to Heaven")
playlist.append("Hotel California")
# Simulate playing
current = playlist.head
print("Now playing:", current.data)
๐ฏ Practice Exercises
Implement a method to delete a node by value
Create a circular doubly linked list
Implement a queue using a doubly linked list
Write a function to find the middle element
Develop a method to rotate the list
Remember: Mastering doubly linked lists requires patience, practice, and a clear understanding of pointer manipulations!