24 - JavaScript - Doubly LinkedList

24 - JavaScript - Doubly LinkedList

A doubly linked list is a data structure consisting of a sequence of nodes, each containing a value and two references: one to the previous node and one to the next node. This allows for efficient traversal in both directions, making it useful for a variety of applications. In contrast to a singly linked list, which only has references to the next node, a doubly linked list can be traversed forwards and backwards without having to start from the beginning.

The first node in the list is called the head, and the last node is called the tail. A doubly linked list can be modified by adding or removing nodes from either end or in the middle. For example, a new node can be added to the end of the list by updating the tail node's "next" reference to point to the new node, and the new node's "prev" reference to point to the tail.

Doubly linked lists have many practical applications, such as implementing browser history, text editor undo/redo functionality, or a music playlist. However, they also require more memory than singly linked lists because each node has to store two references instead of one.

Visualization Of Doubly LinkedList

Here's a visual representation of a doubly linked list:

              HEAD                                       TAIL
                ↓                                          ↓
┌─────┐     ┌─────┐     ┌─────┐     ┌─────┐     ┌─────┐     ┌─────┐
│prev │     │prev │     │prev │     │prev │     │prev │     │prev │
│  -  ├────►│  A  ├────►│  B  ├────►│  C  ├────►│  D  ├────►│  E  │
│next │◄────┤next │◄────┤next │◄────┤next │◄────┤next │◄────┤next │
└─────┘     └─────┘     └─────┘     └─────┘     └─────┘     └─────┘

In a doubly linked list, each node contains a value and two references: one to the previous node and one to the next node. The first node in the list is called the head, and the last node is called the tail.

In the figure above, we have a doubly linked list with five nodes, labeled A through E. The arrows show the references between the nodes. For example, node A has a reference to node B as its next node, and node B has a reference to node A as its previous node.

Note that the references allow for efficient traversal in both directions. For example, if we start at the head of the list and want to move to the next node, we can simply follow the "next" reference. If we want to move to the previous node, we can follow the "prev" reference.

Overall, a doubly linked list is a powerful data structure that can be used in a variety of applications, such as implementing a browser's back and forward buttons or a text editor's undo and redo functionality.

Syntax Of Doubly Linked List

In JavaScript, a doubly linked list can be implemented using a Node class and a LinkedList class. Here's an example of the syntax for the Node class:

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

In JavaScript, a doubly linked list can be implemented using a Node class and a LinkedList class. Here's an example of the syntax for the Node class:

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

The Node class defines a node in the doubly linked list, with a "value" property and two references: "next" points to the next node in the list, and "prev" points to the previous node.

Next, here's an example of the syntax for the LinkedList class:

class LinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  // add methods for adding and removing nodes, etc.
}

The LinkedList class defines the entire list, with a "head" property pointing to the first node in the list, a "tail" property pointing to the last node, and a "length" property keeping track of the number of nodes in the list. The class also includes methods for adding and removing nodes, as well as traversing the list.

These are just examples of the syntax for a doubly linked list in JavaScript. The actual implementation may vary depending on the specific use case and requirements.

Code Examples

  1. Insertion Operation

    Insertion operation in a doubly linked list adds a new node to the beginning of the list.

    The insertAtBeginning method adds a new node to the beginning of the list. It takes a value as an argument and creates a new node with that value. If the list is empty, the new node becomes both the head and tail of the list. Otherwise, the new node is inserted at the beginning of the list by updating the "prev" reference of the current head node to point to the new node, and updating the "next" reference of the new node to point to the current head node. Finally, the head pointer is updated to point to the new node.

     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
         this.prev = null;
       }
     }
    
     class DoublyLinkedList {
       constructor() {
         this.head = null;
         this.tail = null;
         this.length = 0;
       }
    
       // Insert Node at Beginning
       insertAtBeginning(value) {
         const node = new Node(value);
         if (!this.head) {
           this.head = node;
           this.tail = node;
         } else {
           this.head.prev = node;
           node.next = this.head;
           this.head = node;
         }
         this.length++;
         return this;
       }
    
       // Display all the nodes
       displayForward() {
         let current = this.head;
         let result = '';
         while (current !== null) {
            result += `${current.value} -> `;
            current = current.next;
         }
         result += 'null';
         console.log(result);
       }
     }
    

    In the above code, we define a Node class that represents a node in a doubly linked list, with a "value" property and two references: "next" points to the next node in the list, and "prev" points to the previous node.

    We also define a DoublyLinkedList class that represents the entire list, with a "head" property pointing to the first node in the list, a "tail" property pointing to the last node, and a "length" property keeping track of the number of nodes in the list.

    Example:

    Let's assume we have an empty doubly linked list, and we want to add the value "apple" to the beginning of the list using the insertAtBeginning method.

     const list = new DoublyLinkedList();
     list.insertAtBeginning("apple");
     console.log(list);
     list.displayForward();
    

    The output will be:

     DoublyLinkedList {
       head: Node { value: 'apple', next: null, prev: null },
       tail: Node { value: 'apple', next: null, prev: null },
       length: 1
     }
     banana -> apple -> null
    

    In this example, we create a new instance of the DoublyLinkedList class and call the insertAtBeginning method with the value "apple". The method creates a new node with the value "apple" and sets it as both the head and tail of the list. The console.log statement outputs the list object, which contains the new node and its properties.

  2. Deletion Operation

    Deletes an element at the beginning of the list. To delete the first node in the list, we simply update the head pointer to point to the second node in the list.

    The deleteFromBeginning method removes the first node from the list. It returns the deleted node, or undefined if the list is empty. If the list has only one node, both the head and tail pointers are set to null. Otherwise, the head pointer is updated to point to the second node, and the "prev" reference of the new head node is set to null. Finally, the "next" reference of the deleted node is set to null.

     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
         this.prev = null;
       }
     }
    
     class DoublyLinkedList {
       constructor() {
         this.head = null;
         this.tail = null;
         this.length = 0;
       }
    
       // Insert Node at Beginning
       insertAtBeginning(value) {
         const node = new Node(value);
         if (!this.head) {
           this.head = node;
           this.tail = node;
         } else {
           this.head.prev = node;
           node.next = this.head;
           this.head = node;
         }
         this.length++;
         return this;
       }
    
       // Delete From Beginning
       deleteFromBeginning() {
         if (!this.head) {
           return undefined;
         }
         const deletedNode = this.head;
         if (this.length === 1) {
           this.head = null;
           this.tail = null;
         } else {
           this.head = this.head.next;
           this.head.prev = null;
           deletedNode.next = null;
         }
         this.length--;
         return deletedNode;
       }
     }
    

    In the above code, we define a Node class that represents a node in a doubly linked list, with a "value" property and two references: "next" points to the next node in the list, and "prev" points to the previous node.

    We also define a DoublyLinkedList class that represents the entire list, with a "head" property pointing to the first node in the list, a "tail" property pointing to the last node, and a "length" property keeping track of the number of nodes in the list.

    Example:

    Let's assume we have a doubly linked list with three nodes, and we want to remove the first node from the list using the deleteFromBeginning method.

     const list = new DoublyLinkedList();
     list.insertAtBeginning("apple");
     list.insertAtBeginning("banana");
     list.insertAtBeginning("orange");
     list.deleteFromBeginning();
     console.log(list);
    

    The output will be:

     DoublyLinkedList {
       head: Node {
         value: 'banana',
         next: Node { value: 'apple', next: null, prev: [Circular] },
         prev: null
       },
       tail: Node {
         value: 'apple',
         next: null,
         prev: Node { value: 'banana', next: [Circular], prev: null }
       },
       length: 2
     }
    

    In this example, we create a new instance of the DoublyLinkedList class and add three nodes to the list using the insertAtBeginning method. Then, we call the deleteFromBeginning method, which removes the first node "orange" from the list. The console.log statement outputs the updated list object, which contains the remaining nodes and their properties.

  3. Insert Last Operation

    Insert Last operation in a doubly linked list adds a new node at the end of the list.

    The insertAtEnd method adds a new node at the end of the list. It takes a value as a parameter and creates a new Node object with the given value. If the list is empty, the head and tail pointers are set to the new node. Otherwise, the next reference of the current tail node is set to the new node, and the prev reference of the new node is set to the current tail node. Finally, the tail pointer is updated to point to the new node.

     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
         this.prev = null;
       }
     }
    
     class DoublyLinkedList {
       constructor() {
         this.head = null;
         this.tail = null;
         this.length = 0;
       }
    
       // Insert at End
       insertAtEnd(value) {
         const newNode = new Node(value);
         if (!this.head) {
           this.head = newNode;
           this.tail = newNode;
         } else {
           this.tail.next = newNode;
           newNode.prev = this.tail;
           this.tail = newNode;
         }
         this.length++;
         return this;
       }
     }
    

    In the above code, we define a Node class that represents a node in a doubly linked list, with a "value" property and two references: "next" points to the next node in the list, and "prev" points to the previous node.

    We also define a DoublyLinkedList class that represents the entire list, with a "head" property pointing to the first node in the list, a "tail" property pointing to the last node, and a "length" property keeping track of the number of nodes in the list.

    Example:

    Let's assume we have a doubly linked list with two nodes, and we want to add a new node "grape" at the end of the list using the insertAtEnd method.

     const list = new DoublyLinkedList();
     list.insertAtEnd("apple");
     list.insertAtEnd("banana");
     list.insertAtEnd("grape");
     console.log(list);
    

    The output will be:

     DoublyLinkedList {
       head: Node {
         value: 'apple',
         next: Node { value: 'banana', next: [Node], prev: [Circular] },
         prev: null
       },
       tail: Node {
         value: 'grape',
         next: null,
         prev: Node { value: 'banana', next: [Circular], prev: [Node] }
       },
       length: 3
     }
    

    In this example, we create a new instance of the DoublyLinkedList class and add two nodes to the list using the insertAtEnd method. Then, we call the insertAtEnd method again to add a new node "grape" at the end of the list. The console.log statement outputs the updated list object, which contains all three nodes and their properties.

  4. Delete Last Operation

    Delete Last operation in a doubly linked list removes the last node from the list.

    The deleteLast method removes the last node from the list. If the list is empty, it returns undefined. Otherwise, it sets the deletedNode variable to point to the current tail node, and updates the tail pointer to point to the previous node. If the list only had one node, both the head and tail pointers are set to null. Finally, the deleted node's next and prev references are set to null, and the length property is decremented by 1.

     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
         this.prev = null;
       }
     }
    
     class DoublyLinkedList {
       constructor() {
         this.head = null;
         this.tail = null;
         this.length = 0;
       }
    
       // Insert at End
       insertAtEnd(value) {
         const newNode = new Node(value);
         if (!this.head) {
           this.head = newNode;
           this.tail = newNode;
         } else {
           this.tail.next = newNode;
           newNode.prev = this.tail;
           this.tail = newNode;
         }
         this.length++;
         return this;
       }
    
       // Delete Last Node
       deleteLast() {
         if (!this.tail) {
           return undefined;
         }
         const deletedNode = this.tail;
         if (this.length === 1) {
           this.head = null;
           this.tail = null;
         } else {
           this.tail = deletedNode.prev;
           this.tail.next = null;
           deletedNode.prev = null;
         }
         this.length--;
         return deletedNode;
       }
     }
    

    In the above code, we define a Node class that represents a node in a doubly linked list, with a "value" property and two references: "next" points to the next node in the list, and "prev" points to the previous node.

    We also define a DoublyLinkedList class that represents the entire list, with a "head" property pointing to the first node in the list, a "tail" property pointing to the last node, and a "length" property keeping track of the number of nodes in the list.

    Example:

    Let's assume we have a doubly linked list with three nodes, and we want to remove the last node from the list using the deleteLast method.

     const list = new DoublyLinkedList();
     list.insertAtEnd("apple");
     list.insertAtEnd("banana");
     list.insertAtEnd("grape");
     list.deleteLast();
     console.log(list);
    

    The output will be:

     DoublyLinkedList {
       head: Node {
         value: 'apple',
         next: Node { value: 'banana', next: null, prev: [Circular] },
         prev: null
       },
       tail: Node {
         value: 'banana',
         next: null,
         prev: Node { value: 'apple', next: [Circular], prev: null }
       },
       length: 2
     }
    

    In this example, we create a new instance of the DoublyLinkedList class and add three nodes to the list using the insertAtEnd method. Then, we call the deleteLast method to remove the last node "grape" from the list. The console.log statement outputs the updated list object, which contains only the first two nodes and their properties.

  5. Insert After Operation

    Insert After operation in a doubly linked list inserts a new node after a specific node in the list.

    The insertAfter method inserts a new node with the given value after the node with the given key. If the key is not found in the list, it returns false. Otherwise, it creates a new node object, and iterates through the list to find the node with the given key. Once found, it updates the next and prev references of the new node and the adjacent nodes to insert the new node into the list.

     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
         this.prev = null;
       }
     }
    
     class DoublyLinkedList {
       constructor() {
         this.head = null;
         this.tail = null;
         this.length = 0;
       }
    
       // Insert at End
       insertAtEnd(value) {
         const newNode = new Node(value);
         if (!this.head) {
           this.head = newNode;
           this.tail = newNode;
         } else {
           this.tail.next = newNode;
           newNode.prev = this.tail;
           this.tail = newNode;
         }
         this.length++;
         return this;
       }
    
       // Insert After Operation
       insertAfter(value, key) {
         const newNode = new Node(value);
    
         let currentNode = this.head;
         while (currentNode) {
           if (currentNode.value === key) {
             if (currentNode === this.tail) {
               this.tail = newNode;
             } else {
               currentNode.next.prev = newNode;
               newNode.next = currentNode.next;
             }
             currentNode.next = newNode;
             newNode.prev = currentNode;
             this.length++;
             return this;
           }
           currentNode = currentNode.next;
         }
         return false;
       }
     }
    

    In the above code, we define a Node class that represents a node in a doubly linked list, with a "value" property and two references: "next" points to the next node in the list, and "prev" points to the previous node.

    We also define a DoublyLinkedList class that represents the entire list, with a "head" property pointing to the first node in the list, a "tail" property pointing to the last node, and a "length" property keeping track of the number of nodes in the list.

    Example:

    Let's assume we have a doubly linked list with three nodes, and we want to insert a new node with the value "orange" after the node with the value "banana" in the list using the insertAfter method.

     const list = new DoublyLinkedList();
     list.insertAtEnd("apple");
     list.insertAtEnd("banana");
     list.insertAtEnd("grape");
     list.insertAfter("orange", "banana");
     console.log(list);
    

    The output will be:

     DoublyLinkedList {
       head: Node {
         value: 'apple',
         next: Node { value: 'banana', next: [Node], prev: [Circular] },
         prev: null
       },
       tail: Node {
         value: 'grape',
         next: null,
         prev: Node { value: 'orange', next: [Circular], prev: [Node] }
       },
       length: 4
     }
    

    In this example, we create a new instance of the DoublyLinkedList class and add three nodes to the list using the insertAtEnd method. Then, we call the insertAfter method to insert a new node with the value "orange" after the node with the value "banana". The console.log statement outputs the updated list object, which contains four nodes and their properties, including the newly inserted node with the value "orange".

  6. Delete Operation

    Delete operation in a doubly linked list removes a node from the list based on a given key.

    The delete method removes the node with the given key from the list. If the key is not found in the list, it returns false. Otherwise, it iterates through the list to find the node with the given key. Once found, it updates the next and prev references of the adjacent nodes to remove the node from the list.

     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
         this.prev = null;
       }
     }
    
     class DoublyLinkedList {
       constructor() {
         this.head = null;
         this.tail = null;
         this.length = 0;
       }
    
       // Insert at End
       insertAtEnd(value) {
         const newNode = new Node(value);
         if (!this.head) {
           this.head = newNode;
           this.tail = newNode;
         } else {
           this.tail.next = newNode;
           newNode.prev = this.tail;
           this.tail = newNode;
         }
         this.length++;
         return this;
       }
    
       // Delete operation
       delete(key) {
         let currentNode = this.head;
         // if head node itself holds the key to be deleted
         if (currentNode && currentNode.value === key) {
           this.head = currentNode.next;
           if (this.head) {
             this.head.prev = null;
           } else {
             this.tail = null;
           }
           this.length--;
           return this;
         }
         // if the key to be deleted is in between the list
         while (currentNode) {
           if (currentNode.value === key) {
             currentNode.prev.next = currentNode.next;
             if (currentNode.next) {
               currentNode.next.prev = currentNode.prev;
             } else {
               this.tail = currentNode.prev;
             }
             this.length--;
             return this;
           }
           currentNode = currentNode.next;
         }
         return false;
       }
     }
    

    In the above code, we define a Node class that represents a node in a doubly linked list, with a "value" property and two references: "next" points to the next node in the list, and "prev" points to the previous node.

    We also define a DoublyLinkedList class that represents the entire list, with a "head" property pointing to the first node in the list, a "tail" property pointing to the last node, and a "length" property keeping track of the number of nodes in the list.

    Example:

    Let's assume we have a doubly linked list with three nodes, and we want to remove the node with the value "banana" from the list using the delete method.

     const list = new DoublyLinkedList();
     list.insertAtEnd("apple");
     list.insertAtEnd("banana");
     list.insertAtEnd("grape");
     list.delete("banana");
     console.log(list);
    

    The output will be:

     {
       head: { value: 'apple', next: [Object], prev: null },
       tail: { value: 'grape', next: null, prev: [Object] },
       length: 2
     }
    

    In this example, we create a new instance of the DoublyLinkedList class and add three nodes to the list using the insertAtEnd method. Then, we call the delete method to remove the node with the value "banana". The console.log statement outputs the updated list object, which contains two nodes and their properties, without the removed node with the value "banana".

  7. Display Forward Operation

    Display forward operation in a doubly linked list prints all the elements of the list in forward direction, starting from the head node.

    The displayForward method iterates through the list in forward direction, starting from the head node, and prints each node's value using a console.log statement.

     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
         this.prev = null;
       }
     }
    
     class DoublyLinkedList {
       constructor() {
         this.head = null;
         this.tail = null;
         this.length = 0;
       }
    
       // Insert at End
       insertAtEnd(value) {
         const newNode = new Node(value);
         if (!this.head) {
           this.head = newNode;
           this.tail = newNode;
         } else {
           this.tail.next = newNode;
           newNode.prev = this.tail;
           this.tail = newNode;
         }
         this.length++;
         return this;
       }
    
       // Display Forward Operation
       displayForward() {
         let current = this.head;
         let result = '';
         while (current !== null) {
            result += `${current.value} -> `;
            current = current.next;
         }
         result += 'null';
         console.log(result);
       }
     }
    

    In the above code, we define a Node class that represents a node in a doubly linked list, with a "value" property and two references: "next" points to the next node in the list, and "prev" points to the previous node.

    We also define a DoublyLinkedList class that represents the entire list, with a "head" property pointing to the first node in the list, a "tail" property pointing to the last node, and a "length" property keeping track of the number of nodes in the list.

    Example:

    Let's assume we have a doubly linked list with three nodes, and we want to print all the nodes' values in forward direction using the displayForward method.

     const list = new DoublyLinkedList();
     list.insertAtEnd("apple");
     list.insertAtEnd("banana");
     list.insertAtEnd("grape");
     list.displayForward();
    

    The output will be:

     apple -> banana -> grape -> null
    

    In this example, we create a new instance of the DoublyLinkedList class and add three nodes to the list using the insertAtEnd method. Then, we call the displayForward method to print all the nodes' values in forward direction. The console.log statement outputs the values "apple banana grape" separated by a space.

  8. Display Backward Operation

    Display backward operation in a doubly linked list prints all the elements of the list in reverse direction, starting from the tail node.

    The displayBackward method iterates through the list in backward direction, starting from the tail node, and prints each node's value using a console.log statement.

     class Node {
       constructor(value) {
         this.value = value;
         this.next = null;
         this.prev = null;
       }
     }
    
     class DoublyLinkedList {
       constructor() {
         this.head = null;
         this.tail = null;
         this.length = 0;
       }
    
       // Insert at End
       insertAtEnd(value) {
         const newNode = new Node(value);
         if (!this.head) {
           this.head = newNode;
           this.tail = newNode;
         } else {
           this.tail.next = newNode;
           newNode.prev = this.tail;
           this.tail = newNode;
         }
         this.length++;
         return this;
       }
    
       // Display Backward Operation
       displayBackward() {
         let currentNode = this.tail;
         let output = '';
         while (currentNode) {
           output += `${currentNode.value} -> `;
           currentNode = currentNode.prev;
         }
         output += 'null';
         console.log(output);
       }
     }
    

    In the above code, we define a Node class that represents a node in a doubly linked list, with a "value" property and two references: "next" points to the next node in the list, and "prev" points to the previous node.

    We also define a DoublyLinkedList class that represents the entire list, with a "head" property pointing to the first node in the list, a "tail" property pointing to the last node, and a "length" property keeping track of the number of nodes in the list.

    Example:

    Let's assume we have a doubly linked list with three nodes, and we want to print all the nodes' values in backward direction using the displayBackward method.

     const list = new DoublyLinkedList();
     list.insertAtEnd("apple");
     list.insertAtEnd("banana");
     list.insertAtEnd("grape");
     list.displayBackward();
    

    The output will be:

     grape -> banana -> apple -> null
    

    In this example, we create a new instance of the DoublyLinkedList class and add three nodes to the list using the insertAtEnd method. Then, we call the displayBackward method to print all the nodes' values in backward direction. The console.log statement outputs the values "grape banana apple" separated by a space.

Summarizing Up

A doubly linked list is a data structure that consists of a sequence of nodes, where each node has a value and two pointers that reference the previous and next nodes in the sequence. This means that the list can be traversed in both forward and backward directions.

The main advantage of a doubly linked list over a singly linked list is that it allows for more efficient deletion of nodes, as it can be done in constant time without iterating through the list. Insertion and deletion of nodes can be performed at both ends of the list as well as at any position in the list, allowing for greater flexibility in manipulating the data.

Basic operations that can be performed on a doubly linked list include insertion, deletion, insertion at the end, deletion from the end, insertion after a specified node, deletion of a specified node, and display of the list in both forward and backward directions. These operations make doubly linked lists useful in a variety of applications where fast and efficient data manipulation is required.

Did you find this article valuable?

Support Bosonique ITEdTech by becoming a sponsor. Any amount is appreciated!