In this article we’ll have a look at a data structure known as a Doubly Linked List. If you’re unfamiliar with Linked Lists, I highly recommend that you check out this tutorial on Linked Lists before proceeding.

A Doubly Linked Lists (often abbreviated as DLL), is very much alike a regular Singly Linked Lists (SLL).
Both DLL and SLL contain a pointer to the next node, as well as a data field to represent the actual value stored in the node.
However, the difference between DLL and SLL is that the Doubly Linked List also contain a pointer to the previous node, not just the next node.

Unlike nodes in a Singly Linked List, nodes in a Doubly Linked List are aware of both the next and the previous node.

The difference between DLL and SLL is visualized in the picture below.

Singly Linked List and Doubly Linked List

We now know that a node in a Doubly Linked List must contain three variables:

  • A variable containing the actual data.
  • A variable storing the pointer to the next node.
  • A variable storing the pointer to the previous node.

With this information in hand, we can now create the ListNode class.

package DoublyLinkedList;

/**
 * This class represents a node in a Doubly Linked List.
 * The next-variable is a pointer to the next node,
 * and the prev-variable is a pointer to the previous node.
 * <p>
 *
 * @author Anders Engen Olsen
 * @see DoublyLinkedList
 */

class ListNode<AnyType> {

    // The actual data
    AnyType data;
    // Reference to the next node
    ListNode<AnyType> next;
    // Reference to the prev node
    ListNode<AnyType> prev;

    /**
     * Constructor.
     * Note that the next and prev variables are set to null, thus this is the "root-node"
     *
     * @param data node data
     */
    ListNode(AnyType data) {
        this(null, data, null);
    }

    /**
     * Constructor.
     *
     * @param data node data
     * @param next reference to next node
     * @param prev reference to the previous node
     */
    ListNode(ListNode<AnyType> prev, AnyType data, ListNode<AnyType> next) {
        this.data = data;
        this.next = next;
        this.prev = prev;
    }
}

Okay, we’ve now made a ListNode which has a pointer to both the previous and the next node. But why? What are the advantages of a doubly linked list?

  • With a doubly linked list, we can iterate both forwards and backwards through the list.
  • The delete operation is much more efficient.
  • Insertion before a node is also much more efficient.

Now, as always, there’s also some drawbacks:

  • Nodes in a DLL have pointers to both the previous and the next node. This requires more memory space.
  • For every operation, we’ll have to update both the previous and the next pointer. More work, basically.

Let’s get started with the actual Linked List. The methods we’ll support in our implementation is as follows:

  • addFront: Inserting element at the front of the list.
  • addEnd: Inserting element at the end of the list.
  • remove: Removing given element from the list.
  • addBefore: Inserting before given element.
  • addAfter: Inserting after given element.
  • isEmpty: Checking whether the list is empty.
  • size: Returning number of elements in the list.

Note that our example won’t allow for duplicates!

Below is a skeleton of our DoublyLinkedList class.

package DoublyLinkedList;

public class DoublyLinkedList<AnyType> {

    // Front / head node
    private ListNode<AnyType> front;

    // Helper, keeping track of size.
    private int size;

    /**
     * Constructing empty list.
     */
    public DoublyLinkedList() {
        front = null;
        size = modificationCount = 0;
    }

    public void addFront(AnyType x) {
    }

    public void addEnd(AnyType x) {

    }

    public void addBefore(AnyType x, AnyType y) {

    }

    public void addAfter(AnyType x, AnyType y) {

    }

    public void remove(AnyType x) {

    }

    public boolean isEmpty() {

    }

    public int size() {

    }
}

We’ll now go through each of the methods, one at a time.

Starting of with the simplest one, size(). Simply return the size variable.
While you’re at it, you can also implement isEmpty. Return true if size is 0, otherwise false.

public boolean isEmpty() {
    return size == 0;
}

public int size() {
    return size;
}

Let’s move on to the method addFront. There are two separate scenarios we’ll have to deal with:

  • Empty list: Simply initiate the front variable as a new ListNode.
  • Non-empty list: Store the current front node in a temp variable. The new front.next will point to the previous front.
    /**
     * Adding a node to the front of the list.
     *
     * @param x Value to add
     */
    public void addFront(AnyType x) {
        if (isEmpty())
            front = new ListNode<AnyType>(x);
        else {
            ListNode<AnyType> temp = front;
            front = new ListNode<AnyType>(null, x, temp);
            front.next.prev = front;
        }
        size++;
    }

Adding a node to the end of the list is somewhat similar. The two scenarios are the same:

  • Empty list: Simply initiate the front variable as a new ListNode.
  • Non-empty list: Traverse the list until the end. Make the last-node.next  point the the new node. Remember to update the previous pointer for the inserted node!
    /**
     * Inserting a node at the end of the list.
     *
     * @param x Value to add.
     */
    public void addEnd(AnyType x) {
        if (isEmpty())
            front = new ListNode<AnyType>(x);
        else {
            ListNode<AnyType> temp = front;
            // Traverse till end of list
            while (temp.next != null) {
                temp = temp.next;
            }
            temp.next = new ListNode<AnyType>(temp, x, null);
        }
        size++;
    }

Adding before a node is a little bit trickier.
First, we loop through the list. If we reach null, the value is not found, and an exception is thrown.
Otherwise, we create a new node:

  • The new node’s previous will be current.previous. New node’s next is current. In other words, we’re inserting between.
  • If the previous node of current is not null, we have to update it’s next pointer.
  • If current.previous is null, we’re at the front. Simply update front to the new node.
  • Lastly, we’ll have to update current.previous to point to the new node.
    /**
     * Adding node before another node
     *
     * @param x Value to look for, adding before x if found
     * @param y Value to add.
     */
    public void addBefore(AnyType x, AnyType y) {
        // List is empty, can't add
        if (isEmpty())
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        ListNode<AnyType> current = front;

        // Looping through until found
        while (current != null && !current.data.equals(x))
            current = current.next;

        // If null, not found
        if (current == null)
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        ListNode<AnyType> newNode = new ListNode<AnyType>(current.prev, y, current);
        if (current.prev != null)
            current.prev.next = newNode;
        else
            front = newNode;
        current.prev = newNode;

        size++;
    }

Adding a node after another is very similar to adding before.

  • Create a new node. Make new node’s previous pointer point to current. The next pointer for the new node will be current.next.
  • If current.next not equals to null, we make current.next.prev point to the new node.
  • Lastly, we update current.next to point to the new node.
    /**
     * Adding node after another node
     *
     * @param x Value to look for, adding after x if found
     * @param y Value to add.
     */
    public void addAfter(AnyType x, AnyType y) {
        if (isEmpty())
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        ListNode<AnyType> current = front;

        // Looping through until found
        while (current != null && !current.data.equals(x))
            current = current.next;

        // If null, not found
        if (current == null)
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        // Not null, value found
        ListNode<AnyType> newNode = new ListNode<AnyType>(current, y, current.next);
        if (current.next != null)
            current.next.prev = newNode;
        current.next = newNode;

        size++;
    }

The last operation we’ll implement is remove.

  • Check if it is the front node. If so, just make front point to front.next.
  • If current.next not equals to null (i.e. not the last node), make current.next.prev point to current.prev.
  • Make current.prev.next point to current.next
/**
     * Removing a Node from the list.
     *
     * @param x Value to remove
     */
    public void remove(AnyType x) {
        if (isEmpty())
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        // Removing front element
        if (front.data.equals(x)) {
            front = front.next;
            return;
        }

        ListNode<AnyType> current = front;

        // Looping through until found
        while (current != null && !current.data.equals(x))
            current = current.next;

        // If null, not found
        if (current == null)
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        // It has a next pointer, so not the last node.
        if (current.next != null)
            current.next.prev = current.prev;

        current.prev.next = current.next;

        size--;
    }

The entire complete class can be found beneath, including JUnit tests!

package DoublyLinkedList;

/**
 * This class represents a node in a Doubly Linked List.
 * The next-variable is a pointer to the next node,
 * and the prev-variable is a pointer to the previous node.
 * <p>
 *
 * @author Anders Engen Olsen
 * @see DoublyLinkedList
 */

public class ListNode<AnyType> {

    // The actual data
    AnyType data;
    // Reference to the next node
    ListNode<AnyType> next;
    // Reference to the prev node
    ListNode<AnyType> prev;

    /**
     * Constructor.
     * Note that the next and prev variables are set to null, thus this is the "root-node"
     *
     * @param data node data
     */
    ListNode(AnyType data) {
        this(null, data, null);
    }

    /**
     * Constructor.
     *
     * @param data node data
     * @param next reference to next node
     * @param prev reference to the previous node
     */
    ListNode(ListNode<AnyType> prev, AnyType data, ListNode<AnyType> next) {
        this.data = data;
        this.next = next;
        this.prev = prev;
    }
}
package DoublyLinkedList;

import java.util.NoSuchElementException;

public class DoublyLinkedList<AnyType> {

    // Front / head node
    private ListNode<AnyType> front;

    // Helper, keeping track of size.
    private int size;

    /**
     * Constructing empty list.
     */
    public DoublyLinkedList() {
        front = null;
    }

    /**
     * Adding a node to the front of the list.
     *
     * @param x Value to add
     */
    public void addFront(AnyType x) {
        if (isEmpty())
            front = new ListNode<AnyType>(x);
        else {
            ListNode<AnyType> temp = front;
            front = new ListNode<AnyType>(null, x, temp);
            front.next.prev = front;
        }
        size++;
    }

    /**
     * Inserting a node at the end of the list.
     *
     * @param x Value to add.
     */
    public void addEnd(AnyType x) {
        if (isEmpty())
            front = new ListNode<AnyType>(x);
        else {
            ListNode<AnyType> temp = front;
            // Traverse till end of list
            while (temp.next != null) {
                temp = temp.next;
            }
            temp.next = new ListNode<AnyType>(temp, x, null);
        }
        size++;
    }

    /**
     * Adding node before another node
     *
     * @param x Value to look for, adding before x if found
     * @param y Value to add.
     */
    public void addBefore(AnyType x, AnyType y) {
        // List is empty, can't add
        if (isEmpty())
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        ListNode<AnyType> current = front;

        // Looping through until found
        while (current != null && !current.data.equals(x))
            current = current.next;

        // If null, not found
        if (current == null)
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        ListNode<AnyType> newNode = new ListNode<AnyType>(current.prev, y, current);
        if (current.prev != null)
            current.prev.next = newNode;
        else
            front = newNode;
        current.prev = newNode;

        size++;
    }

    /**
     * Adding node after another node
     *
     * @param x Value to look for, adding after x if found
     * @param y Value to add.
     */
    public void addAfter(AnyType x, AnyType y) {
        if (isEmpty())
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        ListNode<AnyType> current = front;

        // Looping through until found
        while (current != null && !current.data.equals(x))
            current = current.next;

        // If null, not found
        if (current == null)
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        // Not null, value found
        ListNode<AnyType> newNode = new ListNode<AnyType>(current, y, current.next);
        if (current.next != null)
            current.next.prev = newNode;
        current.next = newNode;

        size++;
    }

    /**
     * Removing a Node from the list.
     *
     * @param x Value to remove
     */
    public void remove(AnyType x) {
        if (isEmpty())
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        // Removing front element
        if (front.data.equals(x)) {
            front = front.next;
            return;
        }

        ListNode<AnyType> current = front;

        // Looping through until found
        while (current != null && !current.data.equals(x))
            current = current.next;

        // If null, not found
        if (current == null)
            throw new NoSuchElementException("Element " + x.toString() + " not found");

        // It has a next pointer, so not the last node.
        if (current.next != null)
            current.next.prev = current.prev;

        current.prev.next = current.next;

        size--;
    }

    /**
     * @return true if list is empty.
     */
    public boolean isEmpty() {
        return size == 0;
    }

    /**
     * @return size of list
     */
    public int size() {
        return size;
    }

    @Override
    public String toString() {
        ListNode<AnyType> temp = front;
        StringBuilder builder = new StringBuilder("[");

        while (temp != null) {
            builder.append(temp.data).append(",");
            temp = temp.next;
        }
        builder.deleteCharAt(builder.length() - 1);
        builder.append("]");
        return builder.toString();
    }
}
import DoublyLinkedList.DoublyLinkedList;
import org.junit.Before;
import org.junit.Test;

import java.util.NoSuchElementException;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class DoublyLinkedListTest {

    private DoublyLinkedList<Integer> list;

    @Before
    public void setUp() {
        list = new DoublyLinkedList<Integer>();
    }

    @Test
    public void testIsEmptyReturnsTrue() {
        assertTrue(list.isEmpty());
    }

    @Test
    public void testIsEmptySizeIsZero() {
        assertEquals(0, list.size());
    }

    @Test(expected = NoSuchElementException.class)
    public void testRemoveNotPresentThrowsException() {
        list.addFront(1);
        list.remove(2);
    }

    @Test(expected = NoSuchElementException.class)
    public void testAddBeforeNotFoundThrowsException() {
        list.addFront(1);
        list.addBefore(0, 2);
    }

    @Test(expected = NoSuchElementException.class)
    public void testAddAfterNotFoundThrowsException() {
        list.addFront(1);
        list.addAfter(0, 2);
    }

    /**
     * Output should be: [4,3,2,1,0]
     */
    @Test
    public void testInsertAtFront() {
        for (int i = 0; i < 5; i++) {
            list.addFront(i);
        }
        assertEquals("[4,3,2,1,0]", list.toString());
    }

    /**
     * Output should be: [0,1,2,3,4]
     */
    @Test
    public void testInsertAtEnd() {
        for (int i = 0; i < 5; i++) {
            list.addEnd(i);
        }
        assertEquals("[0,1,2,3,4]", list.toString());
    }

    /**
     * Output should be: [10,4,3,30,2,1,20,0]
     */
    @Test
    public void testAddBefore() {
        for (int i = 0; i < 5; i++) {
            list.addFront(i);
        }

        list.addBefore(4, 10);
        list.addBefore(0, 20);
        list.addBefore(2, 30);

        assertEquals("[10,4,3,30,2,1,20,0]", list.toString());
    }

    /**
     * Output should be: [0,20,1,2,30,3,4,10]
     */
    @Test
    public void testAddAfter() {
        for (int i = 0; i < 5; i++) {
            list.addEnd(i);
        }

        list.addAfter(4, 10);
        list.addAfter(0, 20);
        list.addAfter(2, 30);

        assertEquals("[0,20,1,2,30,3,4,10]", list.toString());
    }

    /**
     * Output should be: [10,11,12,13,14]
     */
    @Test
    public void testRemove() {
        for (int i = 0; i < 15; i++) {
            list.addEnd(i);
        }

        for (int i = 0; i < 10; i++) {
            list.remove(i);
        }

        assertEquals("[10,11,12,13,14]", list.toString());
    }
}