Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -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 <E> the type of elements held in this list
*/
public class CircularDoublyLinkedList<E> {
static final class Node<E> {
Node<E> next;
Node<E> prev;
E value;

private Node(E value, Node<E> next, Node<E> prev) {
this.value = value;
this.next = next;
this.prev = prev;
}
}

private int size;
Node<E> 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<E> 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<E> 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<E> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Integer> 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.");
}
}