DEV Community

Cover image for Deep Dive into Data structures using Javascript - Doubly Linked List
Şahin Arslan
Şahin Arslan

Posted on • Originally published at sahinarslan.tech

Deep Dive into Data structures using Javascript - Doubly Linked List

What is a Doubly Linked List?

A Doubly Linked List is a variation of Linked List data structure. It contains all characteristics of a Singly Linked List (or we simply call it Linked List) with one additional feature: each Node contains 2 pointers (previous and next) unlike Singly Linked List which has only one pointer that points to the next Node.

In this article I will be referring to Singly Linked Lists in some sections, therefore the tone of the article will be assuming you are familiar with the Linked List data structure. If that's not the case or you need a quick refreshment on Linked Lists, I'd suggest you to start from the Linked List article by following the link below, then come back and continue here later:

Deep Dive into Data structures using Javascript - Linked List

Anatomy of a Doubly Linked List

doubly-linked-list-anatomy

A Doubly Linked List is consisted by a series of connected Nodes, each Node contains 3 properties:

Prev (pointer): Holds a reference (pointer) to the previous Node.

Value: Holds the value / data for the Node.

Next (pointer): Holds a reference (pointer) to the next Node.

Similar to Singly Linked List, we also call the first node "HEAD" and the last node "TAIL" here. But you probably have noticed a slight difference with the head node - one part is pointing to null on the visual. Why? It is because since we know the head is always first node - there is no other previous Node in the list to point to. Therefore the previous pointer on the head node will be always pointing to the null.

When and When not to use Doubly Linked List

When you have a situation that you might specifically considering to use a Doubly Linked List, it is most likely you have already decided to use a Linked List - and making comparison between if you go with a Singular Linked List or a Doubly Linked List. On next section we will be comparing these two. But let's start with taking a quick look at the Big O of common operations in Doubly Linked List.

doubly-linked-list-big-o

Doubly Linked List vs Singly Linked List

doubly-vs-singly-linked-list

Whenever we deal with different data structures or their different implementations, answer to what to choose is same: "It depends on the context". To get a better idea, let's take a look at pros and cons of each one.

Singly Linked List

Pros:

  • Implementation is simpler and more straight-forward compared to Doubly Linked List.
  • It requires less memory, due to having a single pointer on each Node.
  • Since we need to deal with a single pointer on each Node, there is less operations inside methods.
  • Due to having less operations on methods, it operates slightly faster than Doubly Linked List.

Cons:

  • Cannot be traversed in reverse direction, because pointers only targets the next Node.
  • If the head node is not maintained correctly and lost for some reason, you will lose the rest of the list in memory.

When to use a Singly Linked List

  • If you have less memory and memory is expensive.
  • Main goal is to do fast insertion & deletion, you don't have to deal with traversal so often.

Doubly Linked List

Pros:

  • Better traversal abilities, it can be traversed in both directions (forward or backward).
  • deleteTail() method is faster. In Singly Linked List, to remove the tail you need to traverse the whole list until the tail node and this operation takes O(n) Linear time. In Doubly Linked List you can simply use the tail Node's previous pointer - which takes O(1) Constant time.

Cons:

  • Implementation is more complex compared to Singly Linked List, due to having 2 pointers to deal with inside the methods.
  • Takes more memory space due to having 2 pointers.
  • It is slightly slower than Singly Linked List due to more operations needed on pointers inside each method.

When to use a Doubly Linked List

  • You don't have a memory problem.
  • You want to do traversals / search elements in the list, ability to traverse backwards will give you better options for optimizing traversal performance.

Doubly Linked List implementation in Javascript

Similar to implementation of Singly Linked List, we will be also using ES6 Classes to build this data structure. If you want, you can open your favorite code editor and follow along with me as we go through the steps.

Step 1 - Build a class for the Doubly Linked List Node

Let’s start with identifying the Node element class, which we can use whenever we need to create a new Node.

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

// Create a new Node:
const newNode = new Node(10)
console.log(newNode)

/* newNode output:
Node {
  value: 10,
  next: null,
  prev: null,
}
*/
Enter fullscreen mode Exit fullscreen mode

Step 2 - Build a class for the Doubly Linked List

Now we can go further and create the class for DoublyLinkedList. We know that there should be head and tail properties. For ease of use, we can as well add a length property to keep track of our list length.

Additionally, we can have an option in the constructor to create the Doubly Linked List empty or with a single starter value. We will be looking at the append method at the next step.

class DoublyLinkedList {
  constructor(value) {
    this.head = null
    this.tail = null
    this.length = 0
  }

  // make it optional to create Doubly Linked List with or without starter value
  if (value) {
    this.append(value)
  }
}

const doublyLinkedList = new DoublyLinkedList()
console.log(doublyLinkedList)

/* doublyLinkedList output at initializing stage (empty starter):

DoublyLinkedList {
  head: null,
  tail: null,
  length: 0
}

*/
Enter fullscreen mode Exit fullscreen mode

At this point we are done with the base building blocks: Node and DoublyLinkedList classes. We can continue with extending our DoublyLinkedList class by introducing common methods. To make these methods easier to understand and reason with, I have placed code comments at specific places inside them.

Here is the list of methods we are going to implement:

  • append(value) - add to the end
  • prepend(value) - add to the beginning
  • toArray() - return Doubly Linked List elements in an array for ease of debugging
  • traverseToIndex(index) - traversal helper
  • insert(index, value) - add to the middle
  • deleteHead() - delete from beginning
  • deleteTail() - delete from the end
  • delete(index) - delete from the middle
  • reverse() - reverse order of items

Step 3 - Doubly Linked List append method

doublylinkedlistappendgif-visualgo

// Add to the end of list
append(value) {
    // Initialize a newNode with value recieved
    const newNode = new Node(value)

    // Let's first check if Doubly Linked List is empty or not.
    if (!this.head) {
        // If there is no head (no elements) it is empty. In that case make the newNode as head
        // since it is the only node at this point and there is no tail either,
        // tail will also have the same value (both head and tail will point to same place in memory from now on):
        this.head = newNode
        this.tail = newNode
    } else {
        // Since the newNode will be the new tail, set the prev value to current tail before applying changes. Timing is important!
        newNode.prev = this.tail
        // we have this.tail = this.head is setup with first entry
        // at first we populate the this.tail.next with newNode. Since both are referencing the same object, both head and tail will look equal at this step:
        this.tail.next = newNode
        // at this step, we cleanup the tail by setting it to newNode. In other words we extended the head by using tail first, then cleaned up the tail by using newNode.
        this.tail = newNode
    }
    this.length++
    return this
}
Enter fullscreen mode Exit fullscreen mode

Step 4 - Doubly Linked List prepend method

doublylinkedlistprependgif-visualgo

// Add to the beginning of list
prepend(value) {
    // Let's check first if Doubly Linked List is empty or not.
    // If that's the case, return here by using the append method instead

    if (!this.head) {
        return this.append(value)
    }

    // Initialize a newNode with value recieved
    const newNode = new Node(value)
    // apply a reference to newNode.next prop. When we add it at the start, naturally prepended node's next value should point to the this.head.
    newNode.next = this.head
    // Since the newNode will be the new previous for the current head, set the prev value of head to be newNode. We do this before changing the pointer of this.head to newNode. Timing is important!
    this.head.prev = newNode
    // now that newNode has the this.head as next and newNode as prev, we can set the this.head as newNode directly.
    this.head = newNode
    this.length++
    return this
}
Enter fullscreen mode Exit fullscreen mode

Step 5 - Doubly Linked List toArray method (optional)

To easily debug what is going on our list (or have an option to output Doubly Linked List as an array), we will need toArray method:

// toArray - loop through nested objects, then return the values in an array
toArray() {
    const array = []
    let currentNode = this.head

    while (currentNode !== null) {
        array.push(currentNode.value)
        currentNode = currentNode.next
    }
    return array
}
Enter fullscreen mode Exit fullscreen mode

Step 6 - Doubly Linked List traverseToIndex method (helper)

Since both insert and removal related methods will have to deal with traversing to a specific index, it will be wise to implement a helper for it:

// lookup / traversal helper
traverseToIndex(index) {
    // validate the received index parameter:
    if (!index) return 'Index is missing'
    if (typeof index !== 'number') return 'Index should be a number'

    let counter = 0
    let currentNode = this.head

    while (counter !== index) {
        currentNode = currentNode.next
        counter++
    }

    return currentNode
}
Enter fullscreen mode Exit fullscreen mode

Step 7 - Doubly Linked List insert method

doublylinkedlistinsertgif-visualgo

insert(index, value) {
    // validate the received index parameter:
    if (!index) return 'Index is missing'
    if (typeof index !== 'number') return 'Index should be a number'

    // if length is too long, just append (add at the end)
    if (index >= this.length || !this.head) {
        return this.append(value)
    }

    // if index is 0, just prepend (add to the beginning)
    if (index === 0) {
        return this.prepend(value)
    }

    // Initialize a newNode with value recieved
    const newNode = new Node(value)

    /*
    Solution flow:
      1 - Pick the previous index Node of target idx
      2 - Pick the target idx Node by using preIdx.next pointer
      3 - Now change previous idx Node pointer to newNode. This will change the previous Node's pointer.
      4 - Now change the newNode.next to targetIdx.
      5 - In other words, we just put the new node in between previous and target: by making previous to point to new node, then new node to previous target idx before insert()
    */

    // previous one
    const preIdx = this.traverseToIndex(index - 1)
    const targetIdx = preIdx.next
    // Set the preIdx next to newNode. This is because newNode replaces the targetIdx's position.
    preIdx.next = newNode
    // Set the newNode prev to preIdx. This is because newNode replaces the targetIdx's position.
    newNode.prev = preIdx
    // Set the newNode next to targetIdx. This is because newNode replaces the targetIdx's position.
    newNode.next = targetIdx
    // Now, targetIdx (which have changed place until this step) will point the prev to the newNode. Again, timing is important on steps!
    targetIdx.prev = newNode
    this.length++
    return this
}
Enter fullscreen mode Exit fullscreen mode

Step 8 - Doubly Linked List deleteHead method

doublylinkedlistdeleteheadgif-visualgo

deleteHead() {
    // check the length - if zero return a warning
    if (this.length === 0) return 'List is empty'

    // If there is only one node left:
    if (this.length === 1) {
        const headVal = this.head.value
        this.head = null
        this.tail = null
        this.prev = null
        this.length--
        return headVal
    }

    // pick the current head value:
    const headVal = this.head.value
    // define newHead as this.head.next
    const newHead = this.head.next
    // make the new heads prev pointer null
    newHead.prev = null
    // now change the head pointer to newHead
    this.head = newHead
    this.length--
    return headVal
}
Enter fullscreen mode Exit fullscreen mode

Step 9 - Doubly Linked List deleteTail method

doublylinkedlistdeletetailgif-visualgo

deleteTail() {
    // check the length - if zero return a warning
    if (this.length === 0) return 'List is empty'

    // If there is only one node left:
    if (this.length === 1) {
        const tailVal = this.tail.value
        this.head = null
        this.tail = null
        this.prev = null
        this.length--
        return tailVal
    }

    // Define new tail by traversing to previous Node of tail idx
    // Note that, tail always points to null. (which is length).
    // length - 1 will point to last Node with a value. Therefore we need to target length - 2
    const tailVal = this.tail.value
    const newTail = this.tail.prev
    // Now, we can just simply update the pointer of newTail to null:
    newTail.next = null
    this.tail = newTail
    this.length--
    return tailVal
}
Enter fullscreen mode Exit fullscreen mode

Step 10 - Doubly Linked List delete method

doublylinkedlistdeletegif-visualgo

delete(index) {
    // validate the received index parameter:
    if (!index) return 'Index is missing'
    if (typeof index !== 'number') return 'Index should be a number'

    // check the length - if zero return a warning
    if (this.length === 0) return 'List is empty'

    // Validation - should not be less than 0
    if (index < 0) return `Minimum idx should be 0 or greater`

    // Check if it is the last element. In that case reset head and tail to null
    if (this.length === 1) {
        this.head = null
        this.tail = null
        this.prev = null
    }

    // If not define removal style. Removal will be either head, middle or tail.
    let removalType

    if (index === 0) {
        removalType = 'head'
    }
    // When we do a removal from middle on Doubly Linked List, we need to take 3 indexes into account: pre, target and next. To be able to make it work the middle removal with the length prop, we specify the comparison one minus form the length prop compared to a Singly Linked List.
    if (index >= this.length - 1) {
        removalType = 'tail'
    }
    if (index > 0 && index < this.length - 1) {
        removalType = 'middle'
    }

    if (removalType === 'head') {
        return this.deleteHead()
    }

    if (removalType === 'tail') {
        return this.deleteTail()
    }

    if (removalType === 'middle') {
        /*
      Pick the previous Node of targetIdx via traverse.
      Pick the target idx with preIdx.next
      Now make preIdx point to targetIdx next. This will remove the node in middle.
    */
        const preIdx = this.traverseToIndex(index - 1)
        const targetIdx = preIdx.next
        const targetVal = targetIdx.value
        const nextIdx = targetIdx.next
        preIdx.next = nextIdx
        nextIdx.prev = preIdx
        this.length--
        return targetVal
    }
}
Enter fullscreen mode Exit fullscreen mode

Final step - Reverse a Doubly Linked List

Similar to reversing a Singly Linked List, we will be also using 3 pointers here to reverse the Doubly Linked List. Strategy is very similar with a minor difference - and that is we already have a previous pointer inside each node here. When we deal with reversing a Singly Linked List, we need to create a pointer instead for previous node while traversing since the nodes does not contain them.

Our goal here is basically changing the direction of pointers, one step at a time:

reverse-doubly-linked-list

reverse() {
  // do not reverse if no elements
    if (this.length === 0) return
  // do not reverse if there is a single element
    if (this.length === 1) return this

    let currNode = this.head
    let prevNode = null
    let nextNode = null

    while (currNode) {
        // Store next node.
        nextNode = currNode.next
        // Store prev node.
        prevNode = currNode.prev

        // Change next node of the current node so it would link to previous node.
        currNode.next = prevNode
        currNode.prev = nextNode

        // Move prevNode and currNode nodes one step forward.
        prevNode = currNode
        currNode = nextNode
    }

    // Set the new tail with this.head (it contains the last item at this point of time):
    this.tail = this.head
     // Now reference this head to previousNode (contains the reversed list):
    this.head = prevNode

    return this
}
Enter fullscreen mode Exit fullscreen mode

I hope this article helped you to understand how Doubly Linked Lists works! I’d also like to encourage you to check out this amazing data structures and algorithms visualizer (I have actually generated the gifs you have seen above at this website): https://visualgo.net/en

You can see the full implementation of the Doubly Linked List in Javascript that we went through in this article below. Thanks for reading!

Implementation of Doubly Linked List in Javascript:

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

class DoublyLinkedList {
    constructor(value) {
        this.head = null
        this.tail = null
        this.length = 0

        // make it optional to create Doubly Linked List with or without starter value
        if (value) {
            this.append(value)
        }
    }

    // Add to the end of list
    append(value) {
        // Initialize a newNode with value recieved
        const newNode = new Node(value)

        // Let's first check if Doubly Linked List is empty or not.
        if (!this.head) {
            // If there is no head (no elements) it is empty. In that case make the newNode as head
            // since it is the only node at this point and there is no tail either,
            // tail will also have the same value (both head and tail will point to same place in memory from now on):
            this.head = newNode
            this.tail = newNode
        } else {
            // Since the newNode will be the new tail, set the prev value to current tail before applying changes. Timing is important!
            newNode.prev = this.tail
            // we have this.tail = this.head is setup with first entry
            // at first we populate the this.tail.next with newNode. Since both are referencing the same object, both head and tail will look equal at this step:
            this.tail.next = newNode
            // at this step, we cleanup the tail by setting it to newNode. In other words we extended the head by using tail first, then cleaned up the tail by using newNode.
            this.tail = newNode
        }
        this.length++
        return this
    }

    // Add to the beginning of list
    prepend(value) {
        // Let's check first if Doubly Linked List is empty or not.
        // If that's the case, return here by using the append method instead

        if (!this.head) {
            return this.append(value)
        }

        // Initialize a newNode with value recieved
        const newNode = new Node(value)
        // apply a reference to newNode.next prop. When we add it at the start, naturally prepended node's next value should point to the this.head.
        newNode.next = this.head
        // Since the newNode will be the new previous for the current head, set the prev value of head to be newNode. We do this before changing the pointer of this.head to newNode. Timing is important!
        this.head.prev = newNode
        // now that newNode has the this.head as next and newNode as prev, we can set the this.head as newNode directly.
        this.head = newNode
        this.length++
        return this
    }

    // toArray - loop through nested objects, then return the values in an array
    toArray() {
        const array = []
        let currentNode = this.head

        while (currentNode !== null) {
            array.push(currentNode.value)
            currentNode = currentNode.next
        }
        return array
    }

    // lookup / traversal helper
    traverseToIndex(index) {
        // validate the received index parameter:
        if (!index) return 'Index is missing'
        if (typeof index !== 'number') return 'Index should be a number'

        let counter = 0
        let currentNode = this.head

        while (counter !== index) {
            currentNode = currentNode.next
            counter++
        }

        return currentNode
    }

    // insert to specific index
    insert(index, value) {
        // validate the received index parameter:
        if (!index) return 'Index is missing'
        if (typeof index !== 'number') return 'Index should be a number'

        // if length is too long, just append (add at the end)
        if (index >= this.length || !this.head) {
            return this.append(value)
        }

        // if index is 0, just prepend (add to the beginning)
        if (index === 0) {
            return this.prepend(value)
        }

        // Initialize a newNode with value recieved
        const newNode = new Node(value)

        /*
        Solution flow:
          1 - Pick the previous index Node of target idx
          2 - Pick the target idx Node by using preIdx.next pointer
          3 - Now change previous idx Node pointer to newNode. This will change the previous Node's pointer.
          4 - Now change the newNode.next to targetIdx.
          5 - In other words, we just put the new node in between previous and target: by making previous to point to new node, then new node to previous target idx before insert()
        */

        // previous one
        const preIdx = this.traverseToIndex(index - 1)
        const targetIdx = preIdx.next
        // Set the preIdx next to newNode. This is because newNode replaces the targetIdx's position.
        preIdx.next = newNode
        // Set the newNode prev to preIdx. This is because newNode replaces the targetIdx's position.
        newNode.prev = preIdx
        // Set the newNode next to targetIdx. This is because newNode replaces the targetIdx's position.
        newNode.next = targetIdx
        // Now, targetIdx (which have changed place until this step) will point the prev to the newNode. Again, timing is important on steps!
        targetIdx.prev = newNode
        this.length++
        return this
    }

    // Delete from beginning of list
    deleteHead() {
        // check the length - if zero return a warning
        if (this.length === 0) return 'List is empty'

        // If there is only one node left:
        if (this.length === 1) {
            const headVal = this.head.value
            this.head = null
            this.tail = null
            this.prev = null
            this.length--
            return headVal
        }

        // pick the current head value:
        const headVal = this.head.value
        // define newHead as this.head.next
        const newHead = this.head.next
        // make the new heads prev pointer null
        newHead.prev = null
        // now change the head pointer to newHead
        this.head = newHead
        this.length--
        return headVal
    }

    // Delete from the end of list
    deleteTail() {
        // check the length - if zero return a warning
        if (this.length === 0) return 'List is empty'

        // If there is only one node left:
        if (this.length === 1) {
            const tailVal = this.tail.value
            this.head = null
            this.tail = null
            this.prev = null
            this.length--
            return tailVal
        }

        // Define new tail by traversing to previous Node of tail idx
        // Note that, tail always points to null. (which is length).
        // length - 1 will point to last Node with a value. Therefore we need to target length - 2
        const tailVal = this.tail.value
        const newTail = this.tail.prev
        // Now, we can just simply update the pointer of newTail to null:
        newTail.next = null
        this.tail = newTail
        this.length--
        return tailVal
    }

    // Delete from specific index
    delete(index) {
        // validate the received index parameter:
        if (!index) return 'Index is missing'
        if (typeof index !== 'number') return 'Index should be a number'

        // check the length - if zero return a warning
        if (this.length === 0) return 'List is empty'

        // Validation - should not be less than 0
        if (index < 0) return `Minimum idx should be 0 or greater`

        // Check if it is the last element. In that case reset head and tail to null
        if (this.length === 1) {
            this.head = null
            this.tail = null
            this.prev = null
        }

        // If not define removal style. Removal will be either head, middle or tail.
        let removalType

        if (index === 0) {
            removalType = 'head'
        }
        // When we do a removal from middle on Doubly Linked List, we need to take 3 indexes into account: pre, target and next. To be able to make it work the middle removal with the length prop, we specify the comparison one minus form the length prop compared to a Singly Linked List.
        if (index >= this.length - 1) {
            removalType = 'tail'
        }
        if (index > 0 && index < this.length - 1) {
            removalType = 'middle'
        }

        if (removalType === 'head') {
            return this.deleteHead()
        }

        if (removalType === 'tail') {
            return this.deleteTail()
        }

        if (removalType === 'middle') {
            /*
          Pick the previous Node of targetIdx via traverse.
          Pick the target idx with preIdx.next
          Now make preIdx point to targetIdx next. This will remove the node in middle.
        */
            const preIdx = this.traverseToIndex(index - 1)
            const targetIdx = preIdx.next
            const targetVal = targetIdx.value
            const nextIdx = targetIdx.next
            preIdx.next = nextIdx
            nextIdx.prev = preIdx
            this.length--
            return targetVal
        }
    }

    // Reverse the list
    reverse() {
        // do not reverse if no elements
        if (this.length === 0) return
        // do not reverse if there is a single element
        if (this.length === 1) return this

        let currNode = this.head
        let prevNode = null
        let nextNode = null

        while (currNode) {
            // Store next node.
            nextNode = currNode.next
            // Store prev node.
            prevNode = currNode.prev

            // Change next node of the current node so it would link to previous node.
            currNode.next = prevNode
            currNode.prev = nextNode

            // Move prevNode and currNode nodes one step forward.
            prevNode = currNode
            currNode = nextNode
        }

        // Set the new tail with this.head (it contains the last item at this point of time):
        this.tail = this.head
        // Now reference this head to previousNode (contains the reversed list):
        this.head = prevNode

        return this
    }
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)