Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 304 additions & 0 deletions data_structures/linked_list/circular_doubly_linked_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
from __future__ import annotations

from collections.abc import Iterator
from dataclasses import dataclass
from typing import Any


@dataclass
class Node:
"""
A node in a circular doubly linked list.
"""
data: Any
next_node: Node | None = None
prev_node: Node | None = None


@dataclass
class CircularDoublyLinkedList:
"""
A circular doubly linked list implementation.
In a circular doubly linked list:
- Each node has references to both next and previous nodes
- The last node's next points to the first node
- The first node's previous points to the last node
"""
head: Node | None = None # Reference to the head (first node)
tail: Node | None = None # Reference to the tail (last node)

def __iter__(self) -> Iterator[Any]:
"""
Iterate through all nodes in the Circular Doubly Linked List yielding their data.
Yields:
The data of each node in the linked list.
"""
if self.head is None:
return

node = self.head
while True:
yield node.data
node = node.next_node
if node == self.head:
break

def __len__(self) -> int:
"""
Get the length (number of nodes) in the Circular Doubly Linked List.
"""
return sum(1 for _ in self)

def __repr__(self) -> str:
"""
Generate a string representation of the Circular Doubly Linked List.
Returns:
A string of the format "1<->2<->.....<->N".
"""
return "<->".join(str(item) for item in iter(self))

def insert_tail(self, data: Any) -> None:
"""
Insert a node with the given data at the end of the Circular Doubly Linked List.
"""
self.insert_nth(len(self), data)

def insert_head(self, data: Any) -> None:
"""
Insert a node with the given data at the beginning of the Circular Doubly Linked List.
"""
self.insert_nth(0, data)

def insert_nth(self, index: int, data: Any) -> None:
"""
Insert the data of the node at the nth position in the Circular Doubly Linked List.
Args:
index: The index at which the data should be inserted.
data: The data to be inserted.

Raises:
IndexError: If the index is out of range.
"""
if index < 0 or index > len(self):
raise IndexError("list index out of range.")

new_node: Node = Node(data)

if self.head is None:
# First node - points to itself in both directions
new_node.next_node = new_node
new_node.prev_node = new_node
self.head = self.tail = new_node
elif index == 0:
# Insert at the head
assert self.tail is not None
new_node.next_node = self.head
new_node.prev_node = self.tail
self.head.prev_node = new_node
self.tail.next_node = new_node
self.head = new_node
elif index == len(self):
# Insert at the tail
assert self.tail is not None
new_node.next_node = self.head
new_node.prev_node = self.tail
self.tail.next_node = new_node
self.head.prev_node = new_node
self.tail = new_node
else:
# Find the position to insert
temp: Node | None = self.head
for _ in range(index):
assert temp is not None
temp = temp.next_node

assert temp is not None
# Insert before temp
prev_node = temp.prev_node
assert prev_node is not None

new_node.next_node = temp
new_node.prev_node = prev_node
temp.prev_node = new_node
prev_node.next_node = new_node

def delete_front(self) -> Any:
"""
Delete and return the data of the node at the front of the Circular Doubly Linked List.
Raises:
IndexError: If the list is empty.
"""
return self.delete_nth(0)

def delete_tail(self) -> Any:
"""
Delete and return the data of the node at the end of the Circular Doubly Linked List.
Returns:
Any: The data of the deleted node.
Raises:
IndexError: If the list is empty.
"""
return self.delete_nth(len(self) - 1)

def delete_nth(self, index: int = 0) -> Any:
"""
Delete and return the data of the node at the nth position in Circular Doubly Linked List.
Args:
index (int): The index of the node to be deleted. Defaults to 0.
Returns:
Any: The data of the deleted node.
Raises:
IndexError: If the index is out of range.
"""
if not 0 <= index < len(self):
raise IndexError("list index out of range.")

assert self.head is not None
assert self.tail is not None

if self.head == self.tail:
# Only one node
delete_node = self.head
self.head = self.tail = None
elif index == 0:
# Delete head node
delete_node = self.head
self.head = self.head.next_node
assert self.head is not None
self.head.prev_node = self.tail
self.tail.next_node = self.head
else:
# Find the node to delete
delete_node: Node | None = self.head
for _ in range(index):
assert delete_node is not None
delete_node = delete_node.next_node

assert delete_node is not None
prev_node = delete_node.prev_node
next_node = delete_node.next_node

assert prev_node is not None
assert next_node is not None

prev_node.next_node = next_node
next_node.prev_node = prev_node

if delete_node == self.tail:
self.tail = prev_node

return delete_node.data

def is_empty(self) -> bool:
"""
Check if the Circular Doubly Linked List is empty.
Returns:
bool: True if the list is empty, False otherwise.
"""
return self.head is None

def traverse_forward(self) -> list[Any]:
"""
Traverse the list in forward direction and return all elements.
Returns:
list: A list containing all elements in forward order.
"""
return list(self)

def traverse_backward(self) -> list[Any]:
"""
Traverse the list in backward direction and return all elements.
Returns:
list: A list containing all elements in backward order.
"""
if self.tail is None:
return []

result = []
node = self.tail
while True:
result.append(node.data)
node = node.prev_node
if node == self.tail:
break
return result


def test_circular_doubly_linked_list() -> None:
"""
Test cases for the CircularDoublyLinkedList class.
>>> test_circular_doubly_linked_list()
"""
circular_doubly_linked_list = CircularDoublyLinkedList()
assert len(circular_doubly_linked_list) == 0
assert circular_doubly_linked_list.is_empty() is True
assert str(circular_doubly_linked_list) == ""

# Test error cases on empty list
try:
circular_doubly_linked_list.delete_front()
raise AssertionError # This should not happen
except IndexError:
assert True # This should happen

try:
circular_doubly_linked_list.delete_tail()
raise AssertionError # This should not happen
except IndexError:
assert True # This should happen

try:
circular_doubly_linked_list.delete_nth(-1)
raise AssertionError
except IndexError:
assert True

try:
circular_doubly_linked_list.delete_nth(0)
raise AssertionError
except IndexError:
assert True

# Test insertions
assert circular_doubly_linked_list.is_empty() is True
for i in range(5):
assert len(circular_doubly_linked_list) == i
circular_doubly_linked_list.insert_nth(i, i + 1)
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(1, 6))

# Test tail and head insertions
circular_doubly_linked_list.insert_tail(6)
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(1, 7))
circular_doubly_linked_list.insert_head(0)
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(7))

# Test deletions
assert circular_doubly_linked_list.delete_front() == 0
assert circular_doubly_linked_list.delete_tail() == 6
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(1, 6))
assert circular_doubly_linked_list.delete_nth(2) == 3

# Test re-insertion
circular_doubly_linked_list.insert_nth(2, 3)
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(1, 6))

assert circular_doubly_linked_list.is_empty() is False

# Test bidirectional traversal
forward = circular_doubly_linked_list.traverse_forward()
backward = circular_doubly_linked_list.traverse_backward()
assert forward == [1, 2, 3, 4, 5]
assert backward == [5, 4, 3, 2, 1]

# Test circular property
if circular_doubly_linked_list.head and circular_doubly_linked_list.tail:
assert circular_doubly_linked_list.head.prev_node == circular_doubly_linked_list.tail
assert circular_doubly_linked_list.tail.next_node == circular_doubly_linked_list.head


if __name__ == "__main__":
import doctest

doctest.testmod()
test_circular_doubly_linked_list()
print("All tests passed!")