A Doubly Linked List is a type of linked list where each node contains two pointers, one pointing to the next node and another pointing to the previous node. This structure allows traversal in both directions, making it more flexible than a singly linked list. In this tutorial, we will explore the core concepts, implementation, operations, and real-world applications of doubly linked lists.
A node in a doubly linked list consists of three components:
class Node {
constructor(data) {
this.data = data;
this.next = null;
this.prev = null;
}
}
A doubly linked list is defined by its head and tail nodes. The head points to the first node, and the tail points to the last node.
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
}
}
To insert a new node at the beginning of the list, update the pointers accordingly:
insertAtBeginning(data) {
const newNode = new Node(data);
if (!this.head) {
this.head = this.tail = newNode;
} else {
newNode.next = this.head;
this.head.prev = newNode;
this.head = newNode;
}
}
To insert a new node at the end, update the tail's pointers:
insertAtEnd(data) {
const newNode = new Node(data);
if (!this.tail) {
this.head = this.tail = newNode;
} else {
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}
}
To insert at a specific position, traverse the list to find the correct node and update pointers:
insertAtPosition(data, position) {
if (position < 0) return false;
const newNode = new Node(data);
let current = this.head;
let index = 0;
while (current && index < position - 1) {
current = current.next;
index++;
}
if (!current) return false; // Position out of bounds
newNode.next = current.next;
newNode.prev = current;
if (current.next) {
current.next.prev = newNode;
} else {
this.tail = newNode; // Inserting at the end
}
current.next = newNode;
}
To delete a node from the beginning, update the head pointer:
deleteFromBeginning() {
if (!this.head) return null;
const deletedNode = this.head;
if (this.head === this.tail) {
this.head = this.tail = null;
} else {
this.head = this.head.next;
this.head.prev = null;
}
return deletedNode.data;
}
To delete a node from the end, update the tail pointer:
deleteFromEnd() {
if (!this.tail) return null;
const deletedNode = this.tail;
if (this.head === this.tail) {
this.head = this.tail = null;
} else {
this.tail = this.tail.prev;
this.tail.next = null;
}
return deletedNode.data;
}
To delete a specific node, update the pointers of its neighbors:
deleteNode(node) {
if (!node || !this.head) return false;
if (node === this.head) {
this.deleteFromBeginning();
} else if (node === this.tail) {
this.deleteFromEnd();
} else {
node.prev.next = node.next;
node.next.prev = node.prev;
}
return true;
}
To traverse the list from head to tail:
forwardTraversal() {
let current = this.head;
while (current) {
console.log(current.data);
current = current.next;
}
}
To traverse the list from tail to head:
backwardTraversal() {
let current = this.tail;
while (current) {
console.log(current.data);
current = current.prev;
}
}
next and prev pointers are correctly updated during insertions and deletions to avoid memory leaks or dangling pointers.Doubly linked lists offer a versatile data structure with the ability to traverse in both directions, making them suitable for various applications where bidirectional traversal is necessary. Understanding their implementation and operations is crucial for effective problem-solving in software development.
By following this comprehensive guide, you should now have a solid understanding of doubly linked lists, including their core concepts, operations, best practices, and real-world applications.