Skip to content

Commit 6b82ee5

Browse files
Add circular doubly linkedlist
1 parent a71618f commit 6b82ee5

1 file changed

Lines changed: 304 additions & 0 deletions

File tree

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Iterator
4+
from dataclasses import dataclass
5+
from typing import Any
6+
7+
8+
@dataclass
9+
class Node:
10+
"""
11+
A node in a circular doubly linked list.
12+
"""
13+
data: Any
14+
next_node: Node | None = None
15+
prev_node: Node | None = None
16+
17+
18+
@dataclass
19+
class CircularDoublyLinkedList:
20+
"""
21+
A circular doubly linked list implementation.
22+
In a circular doubly linked list:
23+
- Each node has references to both next and previous nodes
24+
- The last node's next points to the first node
25+
- The first node's previous points to the last node
26+
"""
27+
head: Node | None = None # Reference to the head (first node)
28+
tail: Node | None = None # Reference to the tail (last node)
29+
30+
def __iter__(self) -> Iterator[Any]:
31+
"""
32+
Iterate through all nodes in the Circular Doubly Linked List yielding their data.
33+
Yields:
34+
The data of each node in the linked list.
35+
"""
36+
if self.head is None:
37+
return
38+
39+
node = self.head
40+
while True:
41+
yield node.data
42+
node = node.next_node
43+
if node == self.head:
44+
break
45+
46+
def __len__(self) -> int:
47+
"""
48+
Get the length (number of nodes) in the Circular Doubly Linked List.
49+
"""
50+
return sum(1 for _ in self)
51+
52+
def __repr__(self) -> str:
53+
"""
54+
Generate a string representation of the Circular Doubly Linked List.
55+
Returns:
56+
A string of the format "1<->2<->.....<->N".
57+
"""
58+
return "<->".join(str(item) for item in iter(self))
59+
60+
def insert_tail(self, data: Any) -> None:
61+
"""
62+
Insert a node with the given data at the end of the Circular Doubly Linked List.
63+
"""
64+
self.insert_nth(len(self), data)
65+
66+
def insert_head(self, data: Any) -> None:
67+
"""
68+
Insert a node with the given data at the beginning of the Circular Doubly Linked List.
69+
"""
70+
self.insert_nth(0, data)
71+
72+
def insert_nth(self, index: int, data: Any) -> None:
73+
"""
74+
Insert the data of the node at the nth position in the Circular Doubly Linked List.
75+
Args:
76+
index: The index at which the data should be inserted.
77+
data: The data to be inserted.
78+
79+
Raises:
80+
IndexError: If the index is out of range.
81+
"""
82+
if index < 0 or index > len(self):
83+
raise IndexError("list index out of range.")
84+
85+
new_node: Node = Node(data)
86+
87+
if self.head is None:
88+
# First node - points to itself in both directions
89+
new_node.next_node = new_node
90+
new_node.prev_node = new_node
91+
self.head = self.tail = new_node
92+
elif index == 0:
93+
# Insert at the head
94+
assert self.tail is not None
95+
new_node.next_node = self.head
96+
new_node.prev_node = self.tail
97+
self.head.prev_node = new_node
98+
self.tail.next_node = new_node
99+
self.head = new_node
100+
elif index == len(self):
101+
# Insert at the tail
102+
assert self.tail is not None
103+
new_node.next_node = self.head
104+
new_node.prev_node = self.tail
105+
self.tail.next_node = new_node
106+
self.head.prev_node = new_node
107+
self.tail = new_node
108+
else:
109+
# Find the position to insert
110+
temp: Node | None = self.head
111+
for _ in range(index):
112+
assert temp is not None
113+
temp = temp.next_node
114+
115+
assert temp is not None
116+
# Insert before temp
117+
prev_node = temp.prev_node
118+
assert prev_node is not None
119+
120+
new_node.next_node = temp
121+
new_node.prev_node = prev_node
122+
temp.prev_node = new_node
123+
prev_node.next_node = new_node
124+
125+
def delete_front(self) -> Any:
126+
"""
127+
Delete and return the data of the node at the front of the Circular Doubly Linked List.
128+
Raises:
129+
IndexError: If the list is empty.
130+
"""
131+
return self.delete_nth(0)
132+
133+
def delete_tail(self) -> Any:
134+
"""
135+
Delete and return the data of the node at the end of the Circular Doubly Linked List.
136+
Returns:
137+
Any: The data of the deleted node.
138+
Raises:
139+
IndexError: If the list is empty.
140+
"""
141+
return self.delete_nth(len(self) - 1)
142+
143+
def delete_nth(self, index: int = 0) -> Any:
144+
"""
145+
Delete and return the data of the node at the nth position in Circular Doubly Linked List.
146+
Args:
147+
index (int): The index of the node to be deleted. Defaults to 0.
148+
Returns:
149+
Any: The data of the deleted node.
150+
Raises:
151+
IndexError: If the index is out of range.
152+
"""
153+
if not 0 <= index < len(self):
154+
raise IndexError("list index out of range.")
155+
156+
assert self.head is not None
157+
assert self.tail is not None
158+
159+
if self.head == self.tail:
160+
# Only one node
161+
delete_node = self.head
162+
self.head = self.tail = None
163+
elif index == 0:
164+
# Delete head node
165+
delete_node = self.head
166+
self.head = self.head.next_node
167+
assert self.head is not None
168+
self.head.prev_node = self.tail
169+
self.tail.next_node = self.head
170+
else:
171+
# Find the node to delete
172+
delete_node: Node | None = self.head
173+
for _ in range(index):
174+
assert delete_node is not None
175+
delete_node = delete_node.next_node
176+
177+
assert delete_node is not None
178+
prev_node = delete_node.prev_node
179+
next_node = delete_node.next_node
180+
181+
assert prev_node is not None
182+
assert next_node is not None
183+
184+
prev_node.next_node = next_node
185+
next_node.prev_node = prev_node
186+
187+
if delete_node == self.tail:
188+
self.tail = prev_node
189+
190+
return delete_node.data
191+
192+
def is_empty(self) -> bool:
193+
"""
194+
Check if the Circular Doubly Linked List is empty.
195+
Returns:
196+
bool: True if the list is empty, False otherwise.
197+
"""
198+
return self.head is None
199+
200+
def traverse_forward(self) -> list[Any]:
201+
"""
202+
Traverse the list in forward direction and return all elements.
203+
Returns:
204+
list: A list containing all elements in forward order.
205+
"""
206+
return list(self)
207+
208+
def traverse_backward(self) -> list[Any]:
209+
"""
210+
Traverse the list in backward direction and return all elements.
211+
Returns:
212+
list: A list containing all elements in backward order.
213+
"""
214+
if self.tail is None:
215+
return []
216+
217+
result = []
218+
node = self.tail
219+
while True:
220+
result.append(node.data)
221+
node = node.prev_node
222+
if node == self.tail:
223+
break
224+
return result
225+
226+
227+
def test_circular_doubly_linked_list() -> None:
228+
"""
229+
Test cases for the CircularDoublyLinkedList class.
230+
>>> test_circular_doubly_linked_list()
231+
"""
232+
circular_doubly_linked_list = CircularDoublyLinkedList()
233+
assert len(circular_doubly_linked_list) == 0
234+
assert circular_doubly_linked_list.is_empty() is True
235+
assert str(circular_doubly_linked_list) == ""
236+
237+
# Test error cases on empty list
238+
try:
239+
circular_doubly_linked_list.delete_front()
240+
raise AssertionError # This should not happen
241+
except IndexError:
242+
assert True # This should happen
243+
244+
try:
245+
circular_doubly_linked_list.delete_tail()
246+
raise AssertionError # This should not happen
247+
except IndexError:
248+
assert True # This should happen
249+
250+
try:
251+
circular_doubly_linked_list.delete_nth(-1)
252+
raise AssertionError
253+
except IndexError:
254+
assert True
255+
256+
try:
257+
circular_doubly_linked_list.delete_nth(0)
258+
raise AssertionError
259+
except IndexError:
260+
assert True
261+
262+
# Test insertions
263+
assert circular_doubly_linked_list.is_empty() is True
264+
for i in range(5):
265+
assert len(circular_doubly_linked_list) == i
266+
circular_doubly_linked_list.insert_nth(i, i + 1)
267+
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(1, 6))
268+
269+
# Test tail and head insertions
270+
circular_doubly_linked_list.insert_tail(6)
271+
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(1, 7))
272+
circular_doubly_linked_list.insert_head(0)
273+
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(7))
274+
275+
# Test deletions
276+
assert circular_doubly_linked_list.delete_front() == 0
277+
assert circular_doubly_linked_list.delete_tail() == 6
278+
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(1, 6))
279+
assert circular_doubly_linked_list.delete_nth(2) == 3
280+
281+
# Test re-insertion
282+
circular_doubly_linked_list.insert_nth(2, 3)
283+
assert str(circular_doubly_linked_list) == "<->".join(str(i) for i in range(1, 6))
284+
285+
assert circular_doubly_linked_list.is_empty() is False
286+
287+
# Test bidirectional traversal
288+
forward = circular_doubly_linked_list.traverse_forward()
289+
backward = circular_doubly_linked_list.traverse_backward()
290+
assert forward == [1, 2, 3, 4, 5]
291+
assert backward == [5, 4, 3, 2, 1]
292+
293+
# Test circular property
294+
if circular_doubly_linked_list.head and circular_doubly_linked_list.tail:
295+
assert circular_doubly_linked_list.head.prev_node == circular_doubly_linked_list.tail
296+
assert circular_doubly_linked_list.tail.next_node == circular_doubly_linked_list.head
297+
298+
299+
if __name__ == "__main__":
300+
import doctest
301+
302+
doctest.testmod()
303+
test_circular_doubly_linked_list()
304+
print("All tests passed!")

0 commit comments

Comments
 (0)