Doubly Linked Lists
A doubly linked list is an enhanced version of a linked list where each node contains references to both the next and previous nodes in the sequence. This bidirectional linking provides additional flexibility but requires more memory per node.
Structure
class Node {
constructor(value) {
this.value = value;
this.next = null;
this.previous = null;
}
}
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
}
Visual Representation
null <- [Prev|Data|Next] <-> [Prev|Data|Next] <-> [Prev|Data|Next] -> null
^ ^
head tail
Advantages Over Singly Linked Lists
- Bidirectional traversal
- More efficient deletion operations
- Can traverse backwards from any node
- O(1) deletion when node reference is known
- Easier implementation of certain algorithms
Disadvantages
- Extra memory for previous pointers
- Additional complexity in implementation
- More complex insertion and deletion operations
Core Operations
Insertion
append(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.previous = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
return this;
}
Deletion
remove(node) {
if (!node.previous) {
// Node is head
this.head = node.next;
} else {
node.previous.next = node.next;
}
if (!node.next) {
// Node is tail
this.tail = node.previous;
} else {
node.next.previous = node.previous;
}
this.length--;
return true;
}
Traversal
// Forward traversal
traverse() {
let current = this.head;
while (current) {
console.log(current.value);
current = current.next;
}
}
// Backward traversal
reverseTraverse() {
let current = this.tail;
while (current) {
console.log(current.value);
current = current.previous;
}
}
Complete Implementation
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
// Add to end
append(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.previous = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
return this;
}
// Add to beginning
prepend(value) {
const newNode = new Node(value);
if (!this.head) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head.previous = newNode;
this.head = newNode;
}
this.length++;
return this;
}
// Delete first node
deleteHead() {
if (!this.head) return null;
const deletedHead = this.head;
if (this.head === this.tail) {
this.head = null;
this.tail = null;
} else {
this.head = this.head.next;
this.head.previous = null;
}
this.length--;
return deletedHead;
}
// Delete last node
deleteTail() {
if (!this.tail) return null;
const deletedTail = this.tail;
if (this.head === this.tail) {
this.head = null;
this.tail = null;
} else {
this.tail = this.tail.previous;
this.tail.next = null;
}
this.length--;
return deletedTail;
}
// Find node by value
find(value) {
let current = this.head;
while (current) {
if (current.value === value) {
return current;
}
current = current.next;
}
return null;
}
}
Time Complexities
Operation | Time Complexity |
---|---|
Access | O(n) |
Search | O(n) |
Insertion | O(1)* |
Deletion | O(1)* |
* When inserting/deleting at a known position. If you need to search first, it becomes O(n).
Space Complexity
O(n) - where n is the number of nodes in the list
Common Use Cases
-
Browser History
- Forward and backward navigation
- Recently visited pages
-
Undo/Redo Operations
- Text editors
- Image editing software
-
Music Players
- Next/previous track functionality
- Playlist navigation
-
Cache Implementation
- LRU (Least Recently Used) Cache
- MRU (Most Recently Used) Cache
Practice Problems
- Implement a music player playlist
- Create a browser history system
- Design an undo/redo mechanism
- Implement LRU cache
- Convert binary tree to doubly linked list