Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 26

Doubly Linked Lists: Implementation and Operations

Recommended resources

Welcome back! In our last lesson, we built a singly linked list from scratch and saw how it works. A key challenge we identified was the inefficiency of some operations, like deleting the last element, which required traversing the entire list (O(N) time) because we could only move forward.

Today, we'll address that limitation by implementing a doubly linked list. Your goal is to implement a doubly linked list with insertion, deletion, and traversal operations in Java. This structure adds a backward reference, unlocking new efficiencies and enabling more complex manipulations, which are common topics in technical interviews.

1. The Doubly Linked List: A Two-Way Street

A doubly linked list enhances the simple node structure we used before. Each node now contains three pieces of information:

  1. Data: The value stored in the node.
  2. Next: A reference to the next node in the sequence.
  3. Prev: A reference to the previous node in the sequence.

This prev pointer is the game-changer. It allows us to traverse the list backward, making operations like deleting the last node much faster. The first node's prev is null, and the last node's next is null.

Doubly Linked List Structure in Java
A doubly linked list. Each node points to both its successor (next) and predecessor (prev). The `head`'s `prev` is `null`, and the `tail`'s `next` is `null`.

To get a formal introduction to this structure and see how it's defined in Java, please read the first part of the following article.

Introduction to Doubly Linked Lists in Java

The article 'Introduction to Doubly Linked Lists in Java' from GeeksforGeeks clearly defines the Node and DoublyLinkedList classes we will be building.

Read the sections titled 'Creation of Doubly Linked Lists in Java'. Focus on the structure of the Node class (with data, prev, next) and the DoublyLinkedList class (with head and tail).


2. Building the Doubly Linked List in Java

Based on the reading, our initial structure in Java will look like this. We'll use a private inner Node class to keep it encapsulated, just as we did with the singly linked list.

public class DoublyLinkedList {

    private Node head;
    private Node tail;
    private int size;

    public DoublyLinkedList() {
        this.size = 0;
    }

    // Our methods will go here.

    private class Node {
        int data;
        Node prev;
        Node next;

        public Node(int data) {
            this.data = data;
        }
    }
}

3. Traversal: The Bidirectional Advantage

One of the most apparent benefits of a doubly linked list is the ability to traverse in both directions. We can start from the head and move forward using next, or start from the tail and move backward using prev.

Introduction to Doubly Linked Lists in Java

The GeeksforGeeks article provides clear implementations for both forward and backward traversal.

Read the section 'Traversing a Doubly Linked List in Java'. Note the simple while loops used for traverseForward and traverseBackward.

Here are the traversal methods we can add to our class:

    public void displayForward() {
        Node temp = head;
        System.out.print("Forward: HEAD -> ");
        while (temp != null) {
            System.out.print(temp.data + " -> ");
            temp = temp.next;
        }
        System.out.println("NULL");
    }

    public void displayBackward() {
        Node temp = tail;
        System.out.print("Backward: TAIL -> ");
        while (temp != null) {
            System.out.print(temp.data + " -> ");
            temp = temp.prev;
        }
        System.out.println("NULL");
    }

4. Insertion Operations

Inserting a node in a doubly linked list requires more pointer adjustments than in a singly linked list. For an insertion between two nodes (say P and N), you must update four references:

  • P.next
  • N.prev
  • The new node's prev
  • The new node's next

Let's explore the three main insertion scenarios.

Introduction to Doubly Linked Lists in Java

This section of the GeeksforGeeks article covers insertion at the beginning, at a specific position, and at the end, complete with diagrams and Java code.

Read the entire section 'Insertion in a Doubly Linked List in Java'. Pay close attention to the diagrams and the pointer updates in the code for each of the three cases.

For a more dynamic, step-by-step explanation of the logic, the "take U forward" video is an excellent supplement.

L3. Introduction to Doubly LinkedList | Insertions and Deletions

Watch these segments from 'Introduction to Doubly LinkedList' by take U forward to see the logic for insertion explained with animations.

Watch the following two parts: Insertion Before Head (44:08 - 47:26): This covers the insertAtBeginning logic. Insertion Before Tail (47:26 - 52:46): This is a bit different from a simple insertAtEnd, but the pointer logic for inserting between the second-to-last and last nodes is highly relevant.

Let's summarize and implement these methods.

insertAtBeginning (O(1))

  1. Create the newNode.
  2. If the list is empty, head and tail both point to newNode.
  3. Otherwise, set newNode.next to the current head, head.prev to newNode, and update head to newNode.

insertAtEnd (O(1))

  1. Create the newNode.
  2. If the list is empty, this is the same as inserting at the beginning.
  3. Otherwise, set tail.next to newNode, newNode.prev to the current tail, and update tail to newNode.

insertAtPosition (O(N))

This is the most complex case. After traversing to the correct position:

  1. Find the node currently at that position (current) and the node before it (previous).
  2. Set newNode.next to current and newNode.prev to previous.
  3. Update previous.next to newNode.
  4. Update current.prev to newNode.

5. Deletion Operations

Deletion is where the prev pointer truly shines. We no longer need to traverse the list to find the second-to-last element to delete the tail.

Introduction to Doubly Linked Lists in Java

The GeeksforGeeks article provides a clear guide to all three deletion scenarios.

Read the section 'Deletion in a Doubly Linked List in Java'. Focus on how the prev pointer simplifies deleting from the end.

Let's break down the logic.

deleteAtBeginning (O(1))

  1. Handle empty and single-node list edge cases.
  2. Set head to head.next.
  3. Set the new head.prev to null.

deleteAtEnd (O(1)) - The big win!

  1. Handle empty and single-node list edge cases.
  2. Set tail to tail.prev.
  3. Set the new tail.next to null.

deleteAtPosition (O(N))

  1. Traverse to find the node to be deleted (current).
  2. The core logic is to "bypass" the node: current.prev.next = current.next;
  3. Then, update the backward link: current.next.prev = current.prev;
Test your understanding!

You have an empty DoublyLinkedList. You perform these operations in order:

  1. insertAtEnd(10)
  2. insertAtEnd(20)
  3. insertAtBeginning(5)
  4. insertAtEnd(30) -> List is now [5, 10, 20, 30]
  5. deleteAtEnd()
  6. deleteAtBeginning()
  7. insertAtPosition(15, 1) (insert 15 at index 1, 0-indexed)

What is the final state of the list (in forward order)? What are the values of head.data and tail.data?

Show answer

Let's trace the state:

  1. insertAtEnd(10): [10]
  2. insertAtEnd(20): [10, 20]
  3. insertAtBeginning(5): [5, 10, 20]
  4. insertAtEnd(30): [5, 10, 20, 30]
  5. deleteAtEnd(): [5, 10, 20] (tail is now 20)
  6. deleteAtBeginning(): [10, 20] (head is now 10)
  7. insertAtPosition(15, 1): The list is [10, 20]. We want to insert 15 at index 1. The node at index 1 is 20. The node before it is 10. The new node 15 goes between them. The final list is [10, 15, 20].

The final list is 10 <-> 15 <-> 20.

  • head.data is 10.
  • tail.data is 20.

6. Complexity Analysis

Let's compare the complexities of singly and doubly linked lists.

Operation Singly Linked List Doubly Linked List Notes
Access (by index) O(N) O(N) Still requires traversal from an end.
Search (by value) O(N) O(N) Traversal is required.
Insertion (at head) O(1) O(1) Constant time pointer updates.
Insertion (at tail) O(1) O(1) Assuming a tail pointer is maintained.
Deletion (at head) O(1) O(1) Constant time pointer updates.
Deletion (at tail) O(N) O(1) This is the key improvement! No traversal needed.

The main trade-offs for a doubly linked list are:

  • Increased Memory: Each node requires extra space for the prev pointer.
  • More Complex Operations: Insertions and deletions require updating more pointers, increasing the chance of bugs if not handled carefully.

Conclusion

You have now learned how to implement a doubly linked list, a fundamental data structure that overcomes a major limitation of its singly linked counterpart.

Key Takeaways:

  • A doubly linked list node contains data, a next pointer, and a prev pointer.
  • The prev pointer enables bidirectional traversal and, most importantly, allows for O(1) deletion from the tail.
  • Insertion and deletion operations in the middle of the list involve updating four pointers, requiring careful implementation.
  • The primary trade-off is slightly higher memory usage per node and more complex pointer management.

Preview of the next lesson:
Now that we have solid implementations of both singly and doubly linked lists, we will tackle a classic interview problem: reversing a linked list. We will explore both iterative and recursive solutions, which are excellent tests of your pointer manipulation skills.