Skip to main content

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โ€‹

  1. Bidirectional traversal
  2. More efficient deletion operations
  3. Can traverse backwards from any node
  4. O(1) deletion when node reference is known
  5. Easier implementation of certain algorithms

Disadvantagesโ€‹

  1. Extra memory for previous pointers
  2. Additional complexity in implementation
  3. 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โ€‹

OperationTime Complexity
AccessO(n)
SearchO(n)
InsertionO(1)*
DeletionO(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โ€‹

  1. Browser History

    • Forward and backward navigation
    • Recently visited pages
  2. Undo/Redo Operations

    • Text editors
    • Image editing software
  3. Music Players

    • Next/previous track functionality
    • Playlist navigation
  4. Cache Implementation

    • LRU (Least Recently Used) Cache
    • MRU (Most Recently Used) Cache

Practice Problemsโ€‹

  1. Implement a music player playlist
  2. Create a browser history system
  3. Design an undo/redo mechanism
  4. Implement LRU cache
  5. Convert binary tree to doubly linked list

Additional Resourcesโ€‹