Doubly Linked Lists
The only difference between a doubly linked list and a singly linked list is that you add a pointer to the previous node as well.
This makes operations like pop much faster because you can traverse backwards.
However, as a result of this extra pointer, doubly linked lists take up more memory.
Constructors
class Node {
  constructor(val) {
    this.val = val
    this.prev = null
    this.next = null
  }
}
class DoublyLinkedList {
  constructor() {
    this.length = 0
    this.head = null
    this.tail = null
  }
}Operations
Push
push(val) {
  const newNode = new Node(val)
  if (length === 0) {
    this.head = newNode
  } else {
    this.tail.next = newNode
    newNode.prev = this.tail
  }
  this.tail = newNode
  this.length++
  return this
}Pop
pop() {
  if (this.length === 0) return undefined
  const poppedNode = this.tail
  if (this.length === 1) {
    this.head = null
    this.tail = null
  } else {
    this.tail = poppedNode.prev
    this.tail.next = null
    poppedNode.prev = null
  }
  this.length--
  return poppedNode
}Unshift
unshift(val) {
  const newNode = new Node(val)
  if (this.length === 0) {
    this.tail = newNode
  } else {
    this.head.prev = newNode
    newNode.next = this.head
  }
  this.head = newNode
  this.length++
  return this
}Shift
shift() {
  if (this.length === 0) return undefined
  const shiftedNode = this.head
  if (this.length === 1) {
    this.head = null
    this.tail = null
  } else {
    this.head = shiftedNode.next
    this.head.prev = null
    shiftedNode.next = null
  }
  this.length--
  return shiftedNode
}Get
The only thing special about get is that because you can traverse forwards or backwards, you can conditionally traverse in either direction depending on if the index is closer to the left or closer to the right. This is a slight optimization that makes get O(n / 2).
get(index) {
  if (index < 0 || index >= this.length) return undefined
  const middle = Math.floor(this.length / 2)
  if (index <= middle) {
    let counter = 0
    let targetNode = this.head
    while (counter < index) {
      targetNode = targetNode.next
      counter++
    }
    return targetNode
  } else {
    let counter = this.length - 1
    let targetNode = this.tail
    while (counter > index) {
      targetNode = targetNode.prev
      counter--
    }
    return targetNode
  }
}Set
set(index, val) {
  const targetNode = this.get(index)
  if (!targetNode) return false
  targetNode.val = val
  return true
}Insert
With insert, the only special thing you're doing is connecting nodes at both ends (prev and next).
insert(index, val) {
  if (index < 0 || index > this.length) return false
  if (index === this.length) {
    this.push(val)
  } else if (index === 0) {
    this.unshift(val)
  } else {
    const newNode = new Node(val)
    const beforeNode = this.get(index - 1)
    const afterNode = beforeNode.next
    // Connect previous node with new node
    beforeNode.next = newNode
    newNode.prev = beforeNode
    // Connect new node with old node
    newNode.next = afterNode
    afterNode.prev = newNode
    this.length++
  }
  return true
}Remove
Just like insert, remove requires connecting the severed ends of each surrounding node.
remove(index) {
  if (index < 0 || index >= this.length) return undefined
  let removedNode
  if (index === this.length - 1) {
    removedNode = this.pop()
  } else if (index === 0) {
    removedNode = this.shift()
  } else {
    removedNode = this.get(index)
    // Connect severed nodes
    removedNode.prev.next = removedNode.next
    removedNode.next.prev = removedNode.prev
    // Clear pointers
    removedNode.prev = null
    removedNode.next = null
    this.length--
  }
  return removedNode
}Reverse
reverse() {
  let node = this.head
  this.head = this.tail
  this.tail = this.head
  let prev = null
  let next
  for(let i = 0; i < this.length; i++) {
    next = node.next
    node.next = prev // <= flipping pointer
    node.prev = next // <= flipping pointer
    prev = node
    node = next
  }
  
  return this
}Big O
Insertion and removal are always O(1) because next and prev make it easy to find the nearby node.
Search and access is still O(n) because you're still traversing the length of the list.
However: As mentioned before, the extra prev pointer means more space complexity.
Last updated