L3: Introduction to Doubly Linked List | Insertions and Deletions
This comprehensive study note explores the transition from singly linked lists to Doubly Linked Lists (DLL), covering node representations in C++ and Java, array-to-DLL conversion, and the complete spectrum of insertion and deletion operations (head, tail, index, and specified node).
1. Why Do We Need a Doubly Linked List? (The Singly Linked List Limitation)
In a Singly Linked List, each node contains a value (data) and a single directional forward pointer (next) pointing to the subsequent node. The final node points to null.
Core Limitation
- Forward-Only Traversal: If you are standing at an arbitrary node in a singly linked list, you cannot look backward because no previous reference exists.
- Finding the previous node requires restarting your search entirely from the
headpointer, resulting in time complexity for backward steps.
2. Core Mental Model: The Doubly Linked List Analogy
Real-Life Analogy: Web Browser History
Real-Life Analogy: Think of a Web Browser History. When you browse from Page A -> Page B -> Page C, you want the ability to click the Forward button to move ahead, but crucially, you also need the Back button to return to your previous page. A Doubly Linked List provides bidirectional movement natively by maintaining two pointers per node: one pointing forward (
next) and one pointing backward (back/prev).
3. Node Class Representation in C++ and Java
To support bidirectional travel, our node structure must be upgraded to hold an additional reference to the previous node.
C++ Struct/Class Representation
class Node {
public:
int data;
Node* next;
Node* back; // Pointer to the previous node
// Constructor with data, next, and back
Node(int val1, Node* next1, Node* back1) {
data = val1;
next = next1;
back = back1;
}
// Constructor with only data (defaults pointers to nullptr)
Node(int val1) {
data = val1;
next = nullptr;
back = nullptr;
}
};
Java Class Representation
class Node {
int data;
Node next;
Node back;
// Constructor with data, next, and back
Node(int data1, Node next1, Node back1) {
this.data = data1;
this.next = next1;
this.back = back1;
}
// Constructor with only data
Node(int data1) {
this.data = data1;
this.next = null;
this.back = null;
}
}
4. Converting an Array into a Doubly Linked List
To convert an array into a DLL:
- Initialize the
headnode using the first array element (array[0]). Itsbackpointer isnull. - Maintain a
previousreference pointer initially pointing tohead. - Iterate from index
1toN-1, instantiating new nodes wherebackpoints to the currentpreviousnode. - Update the
previousnode'snextpointer to link to the new node, then shift thepreviouspointer forward.
C++ Implementation
Node* convertArr2DLL(vector<int>& arr) {
if (arr.empty()) return nullptr;
// Create head using the first element
Node* head = new Node(arr[0]);
Node* prev = head;
for (int i = 1; i < arr.size(); i++) {
// Create temp node: next = nullptr, back = prev
Node* temp = new Node(arr[i], nullptr, prev);
prev->next = temp; // Link previous node forward
prev = temp; // Shift prev forward
}
return head;
}
Java Implementation
private static Node convertArr2DLL(int[] arr) {
if (arr.length == 0) return null;
Node head = new Node(arr[0]);
Node prev = head;
for (int i = 1; i < arr.length; i++) {
Node temp = new Node(arr[i], null, prev);
prev.next = temp;
prev = temp;
}
return head;
}
5. Deletion Operations in Doubly Linked List
A. Deleting the Head Node
Edge Cases:
- Empty List (
head == nullptr): Cannot delete; returnnull. - Single Element (
head->next == nullptr): Delete the single node and returnnull.
Standard Steps (Multiple Elements):
- Preserve the current
headin aprevpointer. - Advance
head = head->next. - Disconnect the new
head's backward link (head->back = nullptr). - Disconnect the old node's forward link (
prev->next = nullptr). - Free/Delete the old node (in C++).
Node* deleteHead(Node* head) {
if (head == nullptr || head->next == nullptr) {
delete head;
return nullptr;
}
Node* prev = head;
head = head->next;
head->back = nullptr;
prev->next = nullptr;
delete prev;
return head;
}
B. Deleting the Tail Node
Standard Steps:
- Traverse to the last node using a
tailpointer (while (tail->next != nullptr) tail = tail->next;). - Capture the new tail (the node preceding the current tail):
Node* newTail = tail->back;. - Sever connections: Set
newTail->next = nullptrandtail->back = nullptr. - Delete the old tail node.
Node* deleteTail(Node* head) {
if (head == nullptr || head->next == nullptr) {
delete head;
return nullptr;
}
Node* tail = head;
while (tail->next != nullptr) {
tail = tail->next;
}
Node* newTail = tail->back;
newTail->next = nullptr;
tail->back = nullptr;
delete tail;
return head;
}
C. Deleting the Element
Strategy:
- Traverse the list while keeping a running
countuntilcount == K. - Evaluate structural edge cases:
- If , invoke
deleteHead(). - If , invoke
deleteTail().
- If , invoke
- For intermediate nodes, bridge the surrounding pointers:
prevNode = KthNode->backfrontNode = KthNode->nextprevNode->next = frontNodefrontNode->back = prevNode
- Isolate and delete
KthNode.
Node* removeKthElement(Node* head, int k) {
if (head == nullptr) return nullptr;
int count = 0;
Node* kNode = head;
while (kNode != nullptr) {
count++;
if (count == k) break;
kNode = kNode->next;
}
// If k exceeds list length
if (kNode == nullptr) return head;
Node* prevNode = kNode->back;
Node* frontNode = kNode->next;
if (prevNode == nullptr && frontNode == nullptr) {
delete kNode;
return nullptr;
}
else if (prevNode == nullptr) {
return deleteHead(head);
}
else if (frontNode == nullptr) {
return deleteTail(head);
}
prevNode->next = frontNode;
frontNode->back = prevNode;
kNode->next = nullptr;
kNode->back = nullptr;
delete kNode;
return head;
}
D. Deleting a Specified Node (Given Node Reference)
Constraint Note: You are guaranteed that the node provided to you will never be the
headnode, preventing accidental head pointer corruption without extra handling.
void deleteNode(Node* temp) {
Node* prevNode = temp->back;
Node* frontNode = temp->next;
// If temp is the tail node
if (frontNode == nullptr) {
prevNode->next = nullptr;
temp->back = nullptr;
delete temp;
return;
}
// Intermediate node bridging
prevNode->next = frontNode;
frontNode->back = prevNode;
temp->next = nullptr;
temp->back = nullptr;
delete temp;
}
6. Insertion Operations in Doubly Linked List
A. Inserting Before the Head
- Create a
newNodewhosenextpoints to the currentheadand whosebackisnullptr. - Update the current
head'sbackpointer to point tonewNode. - Return
newNodeas the new head of the list.
Node* insertBeforeHead(Node* head, int val) {
Node* newHead = new Node(val, head, nullptr);
if (head != nullptr) {
head->back = newHead;
}
return newHead;
}
B. Inserting Before the Tail
- Traverse to the final node (
tail). - If the list has only 1 element, delegate to
insertBeforeHead(). - Identify the node preceding the tail (
prev = tail->back). - Instantiate
newNodewithnext = tailandback = prev. - Update pointers:
prev->next = newNodeandtail->back = newNode.
Node* insertBeforeTail(Node* head, int val) {
if (head->next == nullptr) {
return insertBeforeHead(head, val);
}
Node* tail = head;
while (tail->next != nullptr) {
tail = tail->next;
}
Node* prev = tail->back;
Node* newNode = new Node(val, tail, prev);
prev->next = newNode;
tail->back = newNode;
return head;
}
C. Inserting Before the Element
- If , insert before head via
insertBeforeHead(). - Traverse to find the node (
temp). - Extract
prev = temp->back. - Create
newNodewithnext = tempandback = prev. - Re-route surrounding references:
prev->next = newNodeandtemp->back = newNode.
Node* insertBeforeKthElement(Node* head, int k, int val) {
if (k == 1) {
return insertBeforeHead(head, val);
}
Node* temp = head;
int count = 0;
while (temp != nullptr) {
count++;
if (count == k) break;
temp = temp->next;
}
if (temp == nullptr) return head; // K out of bounds
Node* prev = temp->back;
Node* newNode = new Node(val, temp, prev);
prev->next = newNode;
temp->back = newNode;
return head;
}
D. Inserting Before a Given Node Reference
void insertBeforeNode(Node* node, int val) {
Node* prev = node->back;
Node* newNode = new Node(val, node, prev);
prev->next = newNode;
node->back = newNode;
}
7. Comparative Operations Matrix
8. Summary and Key Takeaways
- Bidirectional Advantage: Doubly Linked Lists support two-way traversal (
nextandbackpointers) at the cost of additional memory overhead per node. - Head Protection: Operations modifying the beginning of the list (e.g.,
insertBeforeHead) must return the newly updated head reference. - Pointer Precision: In doubly linked list pointer manipulations, always update both directions (forward and backward links) simultaneously to prevent broken chains.
- Garbage Collection: In languages like Java, unreferenced nodes are automatically cleaned up by the garbage collector, whereas C++ requires explicit
deletecalls to prevent memory leaks. - Real-World Utility: DLL mechanics form the foundational data structure behind browser navigation history caches, LRU Caches, and undo-redo functionality in software editors.