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:
- Data: The value stored in the node.
- Next: A reference to the next node in the sequence.
- 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.

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.nextN.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))
- Create the
newNode. - If the list is empty,
headandtailboth point tonewNode. - Otherwise, set
newNode.nextto the currenthead,head.prevtonewNode, and updateheadtonewNode.
insertAtEnd (O(1))
- Create the
newNode. - If the list is empty, this is the same as inserting at the beginning.
- Otherwise, set
tail.nexttonewNode,newNode.prevto the currenttail, and updatetailtonewNode.
insertAtPosition (O(N))
This is the most complex case. After traversing to the correct position:
- Find the node currently at that position (
current) and the node before it (previous). - Set
newNode.nexttocurrentandnewNode.prevtoprevious. - Update
previous.nexttonewNode. - Update
current.prevtonewNode.
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))
- Handle empty and single-node list edge cases.
- Set
headtohead.next. - Set the new
head.prevtonull.
deleteAtEnd (O(1)) - The big win!
- Handle empty and single-node list edge cases.
- Set
tailtotail.prev. - Set the new
tail.nexttonull.
deleteAtPosition (O(N))
- Traverse to find the node to be deleted (
current). - The core logic is to "bypass" the node:
current.prev.next = current.next; - Then, update the backward link:
current.next.prev = current.prev;
Test your understanding!
You have an empty DoublyLinkedList. You perform these operations in order:
insertAtEnd(10)insertAtEnd(20)insertAtBeginning(5)insertAtEnd(30)-> List is now[5, 10, 20, 30]deleteAtEnd()deleteAtBeginning()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:
insertAtEnd(10):[10]insertAtEnd(20):[10, 20]insertAtBeginning(5):[5, 10, 20]insertAtEnd(30):[5, 10, 20, 30]deleteAtEnd():[5, 10, 20](tailis now 20)deleteAtBeginning():[10, 20](headis now 10)insertAtPosition(15, 1): The list is[10, 20]. We want to insert15at index 1. The node at index 1 is20. The node before it is10. The new node15goes between them. The final list is[10, 15, 20].
The final list is 10 <-> 15 <-> 20.
head.datais 10.tail.datais 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
prevpointer. - 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, anextpointer, and aprevpointer. - The
prevpointer enables bidirectional traversal and, most importantly, allows forO(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.