Skip to content
On this page

Table of Contents generated with DocToc

Stack

Conception

A stack is the basic data structure that can be logically thought of as a linear structure.

Insertion and deletion of items at the top of the stack and the operation should obey the rules LIFO(Last In First Out).

Implementation

Each data structure can be implemented by the different method. We can treat stack as a subclass of Array. So we take the array for example here.

js
class Stack {
  constructor() {
    this.stack = []
  }
  push(item) {
    this.stack.push(item)
  }
  pop() {
    this.stack.pop()
  }
  peek() {
    return this.stack[this.getCount() - 1]
  }
  getCount() {
    return this.stack.length
  }
  isEmpty() {
    return this.getCount() === 0
  }
}

Application

We choose the NO.20 topic in LeetCode

Our goal is to match the brackets. We can use the features of the stack to implement it.

js
var isValid = function (s) {
  let map = {
    '(': -1,
    ')': 1,
    '[': -2,
    ']': 2,
    '{': -3,
    '}': 3
  }
  let stack = []
  for (let i = 0; i < s.length; i++) {
    if (map[s[i]] < 0) {
      stack.push(s[i])
    } else {
      let last = stack.pop()
      if (map[last] + map[s[i]] != 0) return false
    }
  }
  if (stack.length > 0) return false
  return true
};

Queues

concept

A queue is a linear data structure. The insertion takes place at one end while the deletion occurs the other one. And the operation should obey the rules FIFO(First In First Out).

implementation

Here, we'll talk two implementations of the queue: Singly-linked Queue and Circular Queue.

Singly-linked Queue

js
class Queue {
  constructor() {
    this.queue = []
  }
  enQueue(item) {
    this.queue.push(item)
  }
  deQueue() {
    return this.queue.shift()
  }
  getHeader() {
    return this.queue[0]
  }
  getLength() {
    return this.queue.length
  }
  isEmpty() {
    return this.getLength() === 0
  }
}

It is an O(n) operation to enqueue in a Singly-linked Queue, while it is an average O(1) in a Circular Queue. So here comes the Circular Queue.

Circular Queue

js
class SqQueue {
  constructor(length) {
    this.queue = new Array(length + 1)
    // head of the queue
    this.first = 0
    // tail of the queue
    this.last = 0
    // size of the queue
    this.size = 0
  }
  enQueue(item) {
    // the array need to expand if last + 1 is the head
    // `% this.queue.length` is to avoid index out of bounds
    if (this.first === (this.last + 1) % this.queue.length) {
      this.resize(this.getLength() * 2 + 1)
    }
    this.queue[this.last] = item
    this.size++
    this.last = (this.last + 1) % this.queue.length
  }
  deQueue() {
    if (this.isEmpty()) {
      throw Error('Queue is empty')
    }
    let r = this.queue[this.first]
    this.queue[this.first] = null
    this.first = (this.first + 1) % this.queue.length
    this.size--
    // if the size of queue is too small 
    // reduce the size half when the real size is quarter of the length and the length is not 2
    if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {
      this.resize(this.getLength() / 2)
    }
    return r
  }
  getHeader() {
    if (this.isEmpty()) {
      throw Error('Queue is empty')
    }
    return this.queue[this.first]
  }
  getLength() {
    return this.queue.length - 1
  }
  isEmpty() {
    return this.first === this.last
  }
  resize(length) {
    let q = new Array(length)
    for (let i = 0; i < length; i++) {
      q[i] = this.queue[(i + this.first) % this.queue.length]
    }
    this.queue = q
    this.first = 0
    this.last = this.size
  }
}

Linked List

Concept

The linked list is a linear data structure and born to be recursive structure. It can fully use the memory of the computer and manage the memory dynamically and flexibly. But Nodes in the linked list must be read in order from the beginning which can be random in the array, and it uses more memory than the array because of the storage used by their pointers.

Implementation

Singly-linked list

javascript
class Node {
  constructor(v, next) {
    this.value = v
    this.next = next
  }
}
class LinkList {
  constructor() {
    // size
    this.size = 0
    // virtual head
    this.dummyNode = new Node(null, null)
  }
  find(header, index, currentIndex) {
    if (index === currentIndex) return header
    return this.find(header.next, index, currentIndex + 1)
  }
  addNode(v, index) {
    this.checkIndex(index)
    // the next of the node inserted should be previous node'next
    // and the previous node's next should point to the node insert,
    // except inserted to tail which next is null
    let prev = this.find(this.dummyNode, index, 0)
    prev.next = new Node(v, prev.next)
    this.size++
    return prev.next
  }
  insertNode(v, index) {
    return this.addNode(v, index)
  }
  addToFirst(v) {
    return this.addNode(v, 0)
  }
  addToLast(v) {
    return this.addNode(v, this.size)
  }
  removeNode(index, isLast) {
    this.checkIndex(index)
    index = isLast ? index - 1 : index
    let prev = this.find(this.dummyNode, index, 0)
    let node = prev.next
    prev.next = node.next
    node.next = null
    this.size--
    return node
  }
  removeFirstNode() {
    return this.removeNode(0)
  }
  removeLastNode() {
    return this.removeNode(this.size, true)
  }
  checkIndex(index) {
    if (index < 0 || index > this.size) throw Error('Index error')
  }
  getNode(index) {
    this.checkIndex(index)
    if (this.isEmpty()) return
    return this.find(this.dummyNode, index, 0).next
  }
  isEmpty() {
    return this.size === 0
  }
  getSize() {
    return this.size
  }
}

Tree

Binary Tree

Binary Tree is a common one of the many structures of the tree. And it is born to be recursive.

Binary tree start at a root node and each node consists of two child-nodes at most: left node and right node. The nodes in the bottom are usually called leaf nodes, and when the leaf nodes is full, we call the Full Binary Tree.

Binary Search Tree

Binary Search Tree (BST) is one of the binary trees, so it has all the features of the binary tree. But different with the binary tree, the value in any node is larger than the values in all nodes in that node's left subtree and smaller than the values in all nodes in that node's right subtree.

This storage method is very suitable for data search. As shown below, when you need to find 6, because the value you need to find is larger than the value of the root node, you only need to find it in the right subtree of the root node, which greatly improves the search efficiency.

Implementation

js
class Node {
  constructor(value) {
    this.value = value
    this.left = null
    this.right = null
  }
}
class BST {
  constructor() {
    this.root = null
    this.size = 0
  }
  getSize() {
    return this.size
  }
  isEmpty() {
    return this.size === 0
  }
  addNode(v) {
    this.root = this._addChild(this.root, v)
  }
  // make comparison to the value of the node when insertion
  _addChild(node, v) {
    if (!node) {
      this.size++
      return new Node(v)
    }
    if (node.value > v) {
      node.left = this._addChild(node.left, v)
    } else if (node.value < v) {
      node.right = this._addChild(node.right, v)
    }
    return node
  }
}

Above is the basic implementation of BST, the implementation of traversing tree are as follows.

There are three ways for traversing trees: Preorder Traversal, In order Traversal, PostOrder Traversal. The difference of these ways is the time when to visit the root node. In the process of traversing the tree, each node traverses three times, traversing itself, traversing the left subtree and traversing the right subtree. If you need to implement pre-order traversal, you only need to operate the first time when traversing to the node.

Following are the implementation by recursive, if you want to find the non-recursive, click here

js
// Preorder traversal can be used to print the structure of the tree
// first root then left, and the right is last
traversal() {
  this._pre(this.root)
}
_pre(node) {
  if (node) {
    console.log(node.value)
    this._pre(node.left)
    this._pre(node.right)
  }
}
// Inorder traversal can be used to order
// you can sort the value of BST only by one time of Inorder traversal
// first left , then root and right is last
midTraversal() {
  this._mid(this.root)
}
_mid(node) {
  if (node) {
    this._mid(node.left)
    console.log(node.value)
    this._mid(node.right)
  }
}
// Postorder traversal can be used in the case that you want to
// operate the child node first and then the parent node
// first left, then right and the root is last
backTraversal() {
  this._back(this.root)
}
_back(node) {
  if (node) {
    this._back(node.left)
    this._back(node.right)
    console.log(node.value)
  }
}

These three ways belong to Deep First Search. Meanwhile, there is Breadth First Search, which traverse the node layer by layer. We can implement it in the queue.

js
breadthTraversal() {
  if (!this.root) return null
  let q = new Queue()
  // enqueue the root node
  q.enQueue(this.root)
  // whether the queue is empty, if true, the traverse is finished.
  while (!q.isEmpty()) {
    // dequeue the head, and whether it has child-node, 
    // if true, enqueue th left and the right
    let n = q.deQueue()
    if (n.left) q.enQueue(n.left)
    if (n.right) q.enQueue(n.right)
  }
}

We will introduce how to find the smallest and the biggest in the tree. Because of the feature of the BST, the smallest must be on the left while the biggest is on the right.

js
getMin() {
  return this._getMin(this.root).value
}
_getMin(node) {
  if (!node.left) return node
  return this._getMin(node.left)
}
getMax() {
  return this._getMax(this.root).value
}
_getMax(node) {
  if (!node.right) return node
  return this._getMin(node.right)
}

Round up and Round down Since these two operations are opposite, the code is similar, here we'll talk about round down. According to the feature of the BST, the target must be on the left. We only need to traverse the left nodes until the current node is no bigger than the target. And then adjudge if there have right nodes, if do, continue the judgment recursively.

js
floor(v) {
  let node = this._floor(this.root, v)
  return node ? node.value : null
}
_floor(node, v) {
  if (!node) return null
  if (node.value === v) return v
  // if the current node is bigger than the target, continue
  if (node.value > v) {
    return this._floor(node.left, v)
  }
  // whether the current node has the right subtree
  let right = this._floor(node.right, v)
  if (right) return right
  return node
}

Rank get the rank of the given value or get the value of the given rank, and these two operations are also similar. We as usual only introduce the operation of the latter. We should retrofit the code to add a property size to each node which indicates how many subnodes a node has, include itself.

js
class Node {
  constructor(value) {
    this.value = value
    this.left = null
    this.right = null
    // add code 
    this.size = 1
  }
}
// add code
_getSize(node) {
  return node ? node.size : 0
}
_addChild(node, v) {
  if (!node) {
    return new Node(v)
  }
  if (node.value > v) {
    // edit code
    node.size++
    node.left = this._addChild(node.left, v)
  } else if (node.value < v) {
    // edit code
    node.size++
    node.right = this._addChild(node.right, v)
  }
  return node
}
select(k) {
  let node = this._select(this.root, k)
  return node ? node.value : null
}
_select(node, k) {
  if (!node) return null
  // get the size of the node in the left subtree
  let size = node.left ? node.left.size : 0
  // if size is bigger than k, the target is in the left side
  if (size > k) return this._select(node.left, k)
  // if the size is smaller than k, the target is in the right side
  // there is need to recalculate the k
  if (size < k) return this._select(node.right, k - size - 1)
  return node
}

Here come the most difficult parts in BST: delete nodes, include the following cases:

  • the target node has no subtree
  • the target node has only one subtree
  • the target node has two subtrees

The first and the second is easy to resolve, while the last is a little difficult. So let us implement the simple operation at first: delete the minimum node. It could not appear in the third case, and the operation delete the largest node is opposite, so there is no need to talk.

js
delectMin() {
  this.root = this._delectMin(this.root)
  console.log(this.root)
}
_delectMin(node) {
  // rescursive  the left subtree
  // if the left subtree is null, check if the right is exist
  // if true, take the right subtree in place of the delect node
  if ((node != null) & !node.left) return node.right
  node.left = this._delectMin(node.left)
  // update the size at last
  node.size = this._getSize(node.left) + this._getSize(node.right) + 1
  return node
}

The last, how to delete a random node. T.Hibbard put forward the solution in 1962 which can be used to solve the third case.

In this situation, we should get the descendant node of the current node which is the smallest node in the current node's right subtree and replace the target node by it. And then assign the descendant node with the subtree of the target, and give the right subtree without decent node to the left subtree.

Since the root node is bigger than all the nodes in left subtree, while less than all the nodes in the right subtree. When you want to delete a root node, you need to pick a suitable node to take place, which should bigger than the root node that means it must come from the right subtree. Then the smallest node would be picked with the limit that all the nodes in the right subtree should bigger than the root node.

js
delect(v) {
  this.root = this._delect(this.root, v)
}
_delect(node, v) {
  if (!node) return null
  // if the target is less than the current node, serach in the left subtree
  if (node.value < v) {
    node.right = this._delect(node.right, v)
  } else if (node.value > v) {
    // if the target is bigger than the current node, serach in the right subtree
    node.left = this._delect(node.left, v)
  } else {
    // in this case, the target has been found
    // check if the node has subtree
    // if true, return the subtree, same operation with `_delectMin`
    if (!node.left) return node.right
    if (!node.right) return node.left
    // in this case, the node has both subtree
    // get the decendent node of the current node, 
    // which is the smallest node in the right subtree
    let min = this._getMin(node.right)
    // delete the smallest after got it
    // Then assign the subtree after deleting the node to the smallest node
    min.right = this._delectMin(node.right)
    // subtree is the same
    min.left = node.left
    node = min
  }
  // update size
  node.size = this._getSize(node.left) + this._getSize(node.right) + 1
  return node
}

AVL Tree

Concept

BST is limited in the production because it is not the strict O(log N) and sometimes it will degenerate to a linked list, e.g., insertion of an ascending order number list.

AVL tree improved the BST, the difference between the left subtree height and the right subtree height in each node is less than 1, which can ensure that the time complexity is strict O(log N). Based on this, the insertion and deletion may need to rotate the tree to balance the height.

Implementation

Since improved from the BST, some codes in AVL are repeated, which we will not analysis again.

Four cases are in the node insertion of AVL tree.

As for l-l(left-left), the new node T1 is in the left side of the node X. The tree cannot keep balance by now, so there need to rotate. After rotating, the tree should still obey the rules the mid is bigger than the left and less than the right according to the features of the BST.

before rotating: T1 < X < T2 < Y < T3 < Z < T4, after rotating, the node Y is the root, so we need to add the right subtree of Y to the left of the Z and update the height of the nodes.

The same situation to the r-r, opposite to the l-l, we do not talk more.

As for the l-r, the new node is on the right side of the node X, and we need to rotate twice.

First, rotate the left node to the left, after that the case change to l-l, we can handle it like l-l.

js
class Node {
  constructor(value) {
    this.value = value
    this.left = null
    this.right = null
    this.height = 1
  }
}

class AVL {
  constructor() {
    this.root = null
  }
  addNode(v) {
    this.root = this._addChild(this.root, v)
  }
  _addChild(node, v) {
    if (!node) {
      return new Node(v)
    }
    if (node.value > v) {
      node.left = this._addChild(node.left, v)
    } else if (node.value < v) {
      node.right = this._addChild(node.right, v)
    } else {
      node.value = v
    }
    node.height =
      1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
    let factor = this._getBalanceFactor(node)
    // when need right-rotate, the height of the left subtree must higher than right 
    if (factor > 1 && this._getBalanceFactor(node.left) >= 0) {
      return this._rightRotate(node)
    }
    // when need left-rotate, the height of the left subtree must lower than right
    if (factor < -1 && this._getBalanceFactor(node.right) <= 0) {
      return this._leftRotate(node)
    }
    // l-r
    // left subtree is higher than right, 
    // and the right subtree of the left subtree of the node 
    // is higher than the left subtree of the left subtree of the node
    if (factor > 1 && this._getBalanceFactor(node.left) < 0) {
      node.left = this._leftRotate(node.left)
      return this._rightRotate(node)
    }
    // r-l
    // left subtree is lower than right, 
    // and the right subtree of the right subtree of the node 
    // is lower than the left subtree of the right subtree of the node
    if (factor < -1 && this._getBalanceFactor(node.right) > 0) {
      node.right = this._rightRotate(node.right)
      return this._leftRotate(node)
    }

    return node
  }
  _getHeight(node) {
    if (!node) return 0
    return node.height
  }
  _getBalanceFactor(node) {
    return this._getHeight(node.left) - this._getHeight(node.right)
  }
  // right-rotate
  //           5                    2
  //         /   \                /   \
  //        2     6   ==>       1      5
  //       /  \               /       /  \
  //      1    3             new     3    6
  //     /
  //    new
  _rightRotate(node) {
    // new root after rotate
    let newRoot = node.left
    // node need to be moved
    let moveNode = newRoot.right
    // right node of the node 2 change to node 5
    newRoot.right = node
    // left node of node 5 change to node 3
    node.left = moveNode
    // update the height
    node.height =
      1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
    newRoot.height =
      1 +
      Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

    return newRoot
  }
  // left-rotate
  //           4                    6
  //         /   \                /   \
  //        2     6   ==>       4      7
  //             /  \         /   \      \
  //            5     7      2     5      new
  //                   \
  //                    new
  _leftRotate(node) {
    // new root after rotate
    let newRoot = node.right
    // node need to be moved
    let moveNode = newRoot.left
    // left node of the node 6 change to node 4
    newRoot.left = node
    // right node of the node 4 change to node 5
    node.right = moveNode
    // update the height
    node.height =
      1 + Math.max(this._getHeight(node.left), this._getHeight(node.right))
    newRoot.height =
      1 +
      Math.max(this._getHeight(newRoot.left), this._getHeight(newRoot.right))

    return newRoot
  }
}

Trie

Concept

In computer science, a trie, also called digital tree and sometimes radix tree or prefix tree (as prefixes can search them), is a kind of search tree—an ordered tree data structure that is used to store a dynamic set or associative array where the keys are usually strings.

Simply, this data structure is used to search string easily, with the following features:

  • the root is on behalf of the empty string, and each node has N links (N is 26 in searching English character), each link represents a character.
  • all nodes do not store a character, and only the path store, this is different from other tree structures.
  • the character in the path from the root to the random node can combine to the strings corresponding to the node

Implementation

Generally, the implementation of the trie is much more simple than others, let's take the English character searching for example.

js
class TrieNode {
  constructor() {
    // the times of each character travels through the node
    this.path = 0
    // the amount of the string to the node
    this.end = 0
    // links
    this.next = new Array(26).fill(null)
  }
}
class Trie {
  constructor() {
    // root node, empty string
    this.root = new TrieNode()
  }
  // insert string
  insert(str) {
    if (!str) return
    let node = this.root
    for (let i = 0; i < str.length; i++) {
      // get the index of the character
      let index = str[i].charCodeAt() - 'a'.charCodeAt()
      // create if without the index
      if (!node.next[index]) {
        node.next[index] = new TrieNode()
      }
      node.path += 1
      node = node.next[index]
    }
    node.end += 1
  }
  // The number of times the search string appears
  search(str) {
    if (!str) return
    let node = this.root
    for (let i = 0; i < str.length; i++) {
      let index = str[i].charCodeAt() - 'a'.charCodeAt()
      // if the index does node exists, there is no string to be search
      if (!node.next[index]) {
        return 0
      }
      node = node.next[index]
    }
    return node.end
  }
  // delete the string
  delete(str) {
    if (!this.search(str)) return
    let node = this.root
    for (let i = 0; i < str.length; i++) {
      let index = str[i].charCodeAt() - 'a'.charCodeAt()
      // if the path is 0,  this means no string pass 
      // delete it
      if (--node.next[index].path == 0) {
        node.next[index] = null
        return
      }
      node = node.next[index]
    }
    node.end -= 1
  }
}

Disjoint Set

Concept

Disjoint Set is a special data structure of the tree. Each node in this structure has a parent node, if there is only the current node, then the pointer of the parent node points to itself.

Two important operations are in this structure,

  • Find: find the member of the set to which the element belongs, and it can be used to determine whether the two elements belong to the same set
  • Union: combine two sets to a new set

Implementation

js
class DisjointSet {
  // init sample
  constructor(count) {
    // each node's parenet node is iteself when initialization
    this.parent = new Array(count)
    // record the deepth of the tree to optimize the complexity of query
    this.rank = new Array(count)
    for (let i = 0; i < count; i++) {
      this.parent[i] = i
      this.rank[i] = 1
    }
  }
  find(p) {
    // check whether the parent node of the current node is itself, if false, means has not found yet
    // uglify the path for optimization
    // assume the parent node of the current node is A
    // mount the current node to the parent node of A to deeply optimize
    while (p != this.parent[p]) {
      this.parent[p] = this.parent[this.parent[p]]
      p = this.parent[p]
    }
    return p
  }
  isConnected(p, q) {
    return this.find(p) === this.find(q)
  }
  // combine
  union(p, q) {
    // find the parent node of the two number
    let i = this.find(p)
    let j = this.find(q)
    if (i === j) return
    // compare the deepth of the two trees 
    // if the deepth is equal, add as you wish
    if (this.rank[i] < this.rank[j]) {
      this.parent[i] = j
    } else if (this.rank[i] > this.rank[j]) {
      this.parent[j] = i
    } else {
      this.parent[i] = j
      this.rank[j] += 1
    }
  }
}

Heap

Concept

Heap is usually treated as a tree-based array list.

It is implemented by constructure binary heap, one of the BST. Features are as follows:

  • each node either larger or less than all its child-nodes
  • heap is always a full-tree

We call the heap Max Binary Heap that its root value is the largest, while the heap with the smallest root value is called Min Binary Heap.

Priority Queue also can be implemented by the heap, with the same operation.

Implementation of Max Binary Heap

The index of the left-child of each node is i * 2 + 1, while the right's is i * 2 + 2, and the parent's is (i - 1) / 2

There are two central operations in the heap, shiftUp and shiftDown. The former is used for insertion, and the latter is to delete the root node.

The key of shiftUp is to compare with the parent node bubbly and exchange the position if it is larger than the parent.

As for shiftDown, first exchange root and the tail node, and then delete the tail. After that, Compare with the parent node and both child-nodes circularly, if the child-node is larger, assign the parent node with the larger node.

js
class MaxHeap {
  constructor() {
    this.heap = []
  }
  size() {
    return this.heap.length
  }
  empty() {
    return this.size() == 0
  }
  add(item) {
    this.heap.push(item)
    this._shiftUp(this.size() - 1)
  }
  removeMax() {
    this._shiftDown(0)
  }
  getParentIndex(k) {
    return parseInt((k - 1) / 2)
  }
  getLeftIndex(k) {
    return k * 2 + 1
  }
  _shiftUp(k) {
    // exchange if the current node is bigger than the parent node
    while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
      this._swap(k, this.getParentIndex(k))
      // update the index to the parent node's
      k = this.getParentIndex(k)
    }
  }
  _shiftDown(k) {
    // exchange the head and tail, then delete the tail
    this._swap(k, this.size() - 1)
    this.heap.splice(this.size() - 1, 1)
    // check whether the node has left child-node, 
    // the right must exist because of full-tree
    while (this.getLeftIndex(k) < this.size()) {
      let j = this.getLeftIndex(k)
      // check whether the right child exits, and whether it is largger than the left
      if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
      // check whether the parenet node is largger than both child-nodes
      if (this.heap[k] >= this.heap[j]) break
      this._swap(k, j)
      k = j
    }
  }
  _swap(left, right) {
    let rightValue = this.heap[right]
    this.heap[right] = this.heap[left]
    this.heap[left] = rightValue
  }
}
js
class MaxHeap {
  constructor() {
    this.heap = []
  }
  size() {
    return this.heap.length
  }
  empty() {
    return this.size() == 0
  }
  add(item) {
    this.heap.push(item)
    this._shiftUp(this.size() - 1)
  }
  removeMax() {
    this._shiftDown(0)
  }
  getParentIndex(k) {
    return parseInt((k - 1) / 2)
  }
  getLeftIndex(k) {
    return k * 2 + 1
  }
  _shiftUp(k) {
    // exchange if the current node is bigger than the parent node
    while (this.heap[k] > this.heap[this.getParentIndex(k)]) {
      this._swap(k, this.getParentIndex(k))
      // update the index to the parent node's
      k = this.getParentIndex(k)
    }
  }
  _shiftDown(k) {
    // exchange the head and delete the tail
    this._swap(k, this.size() - 1)
    this.heap.splice(this.size() - 1, 1)
    // check if the node has left child-node, the right must exist if true according to the binary heap
    while (this.getLeftIndex(k) < this.size()) {
      let j = this.getLeftIndex(k)
      // check if the right child exits, and whether it is largger than the left
      if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++
      // check if the parenet node is largger than both child-nodes
      if (this.heap[k] >= this.heap[j]) break
      this._swap(k, j)
      k = j
    }
  }
  _swap(left, right) {
    let rightValue = this.heap[right]
    this.heap[right] = this.heap[left]
    this.heap[left] = rightValue
  }
}