diff --git a/src/main/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedList.java b/src/main/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedList.java new file mode 100644 index 000000000000..fbb48854c449 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedList.java @@ -0,0 +1,118 @@ +package com.thealgorithms.datastructures.lists; + +/** + * This class is a circular doubly linked list implementation. In a circular + * doubly linked list, + * the last node points back to the first node and the first node points back to + * the last node, + * creating a circular chain in both directions. + * + * This implementation includes basic operations such as appending elements to + * the end, + * removing elements from a specified position, and converting the list to a + * string representation. + * + * @param the type of elements held in this list + */ +public class CircularDoublyLinkedList { + static final class Node { + Node next; + Node prev; + E value; + + private Node(E value, Node next, Node prev) { + this.value = value; + this.next = next; + this.prev = prev; + } + } + + private int size; + Node head = null; + + /** + * Initializes a new circular doubly linked list. A dummy head node is used for + * simplicity, + * pointing initially to itself to ensure the list is never empty. + */ + public CircularDoublyLinkedList() { + head = new Node<>(null, null, null); + head.next = head; + head.prev = head; + size = 0; + } + + /** + * Returns the current size of the list. + * + * @return the number of elements in the list + */ + public int getSize() { + return size; + } + + /** + * Appends a new element to the end of the list. Throws a NullPointerException + * if + * a null value is provided. + * + * @param value the value to append to the list + * @throws NullPointerException if the value is null + */ + public void append(E value) { + if (value == null) { + throw new NullPointerException("Cannot add null element to the list"); + } + Node newNode = new Node<>(value, head, head.prev); + head.prev.next = newNode; + head.prev = newNode; + size++; + } + + /** + * Returns a string representation of the list in the format "[ element1, + * element2, ... ]". + * An empty list is represented as "[]". + * + * @return the string representation of the list + */ + public String toString() { + if (size == 0) { + return "[]"; + } + StringBuilder sb = new StringBuilder("[ "); + Node current = head.next; + while (current != head) { + sb.append(current.value); + if (current.next != head) { + sb.append(", "); + } + current = current.next; + } + sb.append(" ]"); + return sb.toString(); + } + + /** + * Removes and returns the element at the specified position in the list. + * Throws an IndexOutOfBoundsException if the position is invalid. + * + * @param pos the position of the element to remove + * @return the value of the removed element - pop operation + * @throws IndexOutOfBoundsException if the position is out of range + */ + public E remove(int pos) { + if (pos >= size || pos < 0) { + throw new IndexOutOfBoundsException("Position out of bounds"); + } + Node current = head.next; + for (int i = 0; i < pos; i++) { + current = current.next; + } + current.prev.next = current.next; + current.next.prev = current.prev; + E removedValue = current.value; + size--; + return removedValue; + } +} diff --git a/src/test/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedListTest.java b/src/test/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedListTest.java new file mode 100644 index 000000000000..faa2765a3264 --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/lists/CircularDoublyLinkedListTest.java @@ -0,0 +1,120 @@ +package com.thealgorithms.datastructures.lists; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class CircularDoublyLinkedListTest { + + private CircularDoublyLinkedList list; + + @BeforeEach + public void setUp() { + list = new CircularDoublyLinkedList<>(); + } + + @Test + public void testInitialSize() { + assertEquals(0, list.getSize(), "Initial size should be 0."); + } + + @Test + public void testAppendAndSize() { + list.append(10); + list.append(20); + list.append(30); + + assertEquals(3, list.getSize(), "Size after appends should be 3."); + assertEquals("[ 10, 20, 30 ]", list.toString(), "List content should match appended values."); + } + + @Test + public void testRemove() { + list.append(10); + list.append(20); + list.append(30); + + int removed = list.remove(1); + assertEquals(20, removed, "Removed element at index 1 should be 20."); + + assertEquals("[ 10, 30 ]", list.toString(), "List content should reflect removal."); + assertEquals(2, list.getSize(), "Size after removal should be 2."); + + removed = list.remove(0); + assertEquals(10, removed, "Removed element at index 0 should be 10."); + assertEquals("[ 30 ]", list.toString(), "List content should reflect second removal."); + assertEquals(1, list.getSize(), "Size after second removal should be 1."); + } + + @Test + public void testRemoveInvalidIndex() { + list.append(10); + list.append(20); + + assertThrows(IndexOutOfBoundsException.class, () -> list.remove(2), "Removing at invalid index 2 should throw exception."); + assertThrows(IndexOutOfBoundsException.class, () -> list.remove(-1), "Removing at negative index should throw exception."); + } + + @Test + public void testToStringEmpty() { + assertEquals("[]", list.toString(), "Empty list should display as []."); + } + + @Test + public void testSingleElement() { + list.append(10); + + assertEquals(1, list.getSize(), "Size after adding single element should be 1."); + assertEquals("[ 10 ]", list.toString(), "Single element list string should be formatted correctly."); + int removed = list.remove(0); + assertEquals(10, removed, "Removed element should be the one appended."); + assertEquals("[]", list.toString(), "List should be empty after removing last element."); + assertEquals(0, list.getSize(), "Size after removing last element should be 0."); + } + + @Test + public void testNullAppend() { + assertThrows(NullPointerException.class, () -> list.append(null), "Appending null should throw NullPointerException."); + } + + @Test + public void testRemoveLastPosition() { + list.append(10); + list.append(20); + list.append(30); + int removed = list.remove(list.getSize() - 1); + assertEquals(30, removed, "Last element removed should be 30."); + assertEquals(2, list.getSize(), "Size should decrease after removing last element."); + } + + @Test + public void testRemoveFromEmptyThrows() { + assertThrows(IndexOutOfBoundsException.class, () -> list.remove(0), "Remove from empty list should throw."); + } + + @Test + public void testRepeatedAppendAndRemove() { + for (int i = 0; i < 100; i++) { + list.append(i); + } + assertEquals(100, list.getSize()); + + for (int i = 99; i >= 0; i--) { + int removed = list.remove(i); + assertEquals(i, removed, "Removed element should match appended value."); + } + assertEquals(0, list.getSize(), "List should be empty after all removes."); + } + + @Test + public void testToStringAfterMultipleRemoves() { + list.append(1); + list.append(2); + list.append(3); + list.remove(2); + list.remove(0); + assertEquals("[ 2 ]", list.toString(), "ToString should correctly represent remaining elements."); + } +}