Data Structures and Algorithms in JavaScript: Hash Table, Tree, Graph

TheTechnoCult
8 min readDec 18, 2023

--

1. Hash Table

JavaScript provides a built-in Map object that implements a hash table. It has methods specifically designed for handling key-value pairs.

|-------------------|
| Hash Table |
|-------------------|
| 'apple' : 5 |
| 'banana': 8 |
| 'orange': 12 |
|-------------------|
const hashTableMap = new Map([
['apple', 5],
['banana', 8],
['orange', 12]
])

hashTableMap.set('pineapple', 13)

console.log(hashTableMap.get('pineapple')) // Output: 13

console.log(hashTableMap.size) // Output: 4

hashTableMap.delete('banana')

Implementation without Map. Complexity is O(1) constant time.

class HashTable {
constructor() {
// Initialize an empty array to store buckets (each bucket is an array of key-value pairs)
this.buckets = []
}

// Hash function to convert a key into an index
hash(key) {
// For simplicity, this is a basic hash function; in practice, a more sophisticated one would be used
let hashValue = 0
for (let i = 0; i < key.length; i++) {
hashValue += key.charCodeAt(i)
}
return hashValue % 100 // Modulo to keep the index within a reasonable range (e.g., 100 buckets)
}

// Add a key-value pair
insert(key, value) {
const index = this.hash(key)

// If the bucket at the calculated index doesn't exist, create it
if (!this.buckets[index]) {
this.buckets[index] = []
}

// Check if the key already exists in the bucket, and update its value if so
for (const pair of this.buckets[index]) {
if (pair[0] === key) {
pair[1] = value
return
}
}

// If the key doesn't exist in the bucket, add a new key-value pair
this.buckets[index].push([key, value])
}

// Remove a key-value pair
remove(key) {
const index = this.hash(key)

// If the bucket at the calculated index exists, search for the key and remove it
if (this.buckets[index]) {
this.buckets[index] = this.buckets[index].filter(pair => pair[0] !== key);
}
}

// Method to get the value associated with a given key
get(key) {
const index = this.hash(key)

// If the bucket at the calculated index exists, search for the key and return its value
if (this.buckets[index]) {
for (const pair of this.buckets[index]) {
if (pair[0] === key) {
return pair[1]
}
}
}

// Return undefined if the key is not found
return undefined
}
}

// Example usage:
const myHashTable = new HashTable()

// Inserting key-value pairs
myHashTable.insert('apple', 5)
myHashTable.insert('banana', 8)
myHashTable.insert('orange', 12)

// Retrieving values
console.log(myHashTable.get('apple')) // Output: 5
console.log(myHashTable.get('banana')) // Output: 8
console.log(myHashTable.get('grape')) // Output: undefined (key not present)

// Removing a key-value pair
myHashTable.remove('banana')

// Retrieving values after removal
console.log(myHashTable.get('banana')) // Output: undefined (key removed)

2. Tree

Main elements are root, node/child, edge, and leaf.

  • Non-binary Tree
class TreeNode {
constructor(data) {
this.data = data
this.children = []
}

addChild(childNode) {
this.children.push(childNode)
}
}

class Tree {
constructor() {
this.root = null
}

addRoot(data) {
this.root = new TreeNode(data)
}

remove(data) {
if (!this.root) {
return null
}

if (this.root.data === data) {
this.root = null
return
}

const removeNode = (node, targetData) => {
node.children =
node.children
.filter((child) => child.data !== targetData)
node.children.forEach((child) => removeNode(child, targetData))
}

removeNode(this.root, data)
}

// BFS Traversal
traverseBFS(callback) {
if (!this.root) {
return
}

const queue = [this.root]

while (queue.length > 0) {
const currentNode = queue.shift()
callback(currentNode.data)

currentNode.children.forEach((child) => queue.push(child))
}
}

// DFS Traversal
traverseDFS(callback, method = 'pre-order') {
const traversePreOrder = (node) => {
callback(node.data)
node.children.forEach(traversePreOrder)
}

const traversePostOrder = (node) => {
node.children.forEach(traversePostOrder)
callback(node.data)
}

if (!this.root) {
return
}

if (method === 'pre-order') {
traversePreOrder(this.root)
} else if (method === 'post-order') {
traversePostOrder(this.root)
}
}
}

// Example usage:

// Create a tree
const myTree = new Tree()

// Add a root node
myTree.addRoot("Root Node")

// Add children to the root
const child1 = new TreeNode("Child 1")
const child2 = new TreeNode("Child 2")
myTree.root.addChild(child1)
myTree.root.addChild(child2)

// Add children to Child 1
const child1_1 = new TreeNode("Child 1.1")
const child1_2 = new TreeNode("Child 1.2")
child1.addChild(child1_1)
child1.addChild(child1_2)

// Add a child to Child 2
const child2_1 = new TreeNode("Child 2.1")
child2.addChild(child2_1)

// The resulting tree structure looks like this:
// Root Node
// / \
// Child 1 Child 2
// / | \ \
// ... ... ... Child 2.1

console.log(myTree)

// Remove a child
myTree.remove("Child 1.1")

// Traversal

// Breadth-First Traversal
console.log("Breadth-First Traversal:")
myTree.traverseBFS(data => console.log(data))

// Pre-Order Traversal
console.log("Pre-Order Traversal:")
myTree.traverseDFS(data => console.log(data), 'pre-order')

// Post-Order Traversal
console.log("Post-Order Traversal:")
myTree.traverseDFS(data => console.log(data), 'post-order')
class TreeNode {
constructor(value) {
this.value = value
this.left = null
this.right = null
}
}

class BinaryTree {
constructor() {
this.root = null
}

// Insert
insert(value) {
this.root = this._insert(this.root, value);
}

_insert(root, value) {
if (root === null) {
return new TreeNode(value)
}

if (value < root.value) {
root.left = this._insert(root.left, value)
} else if (value > root.value) {
root.right = this._insert(root.right, value)
}

return root
}

// Remove
remove(value) {
this.root = this._remove(this.root, value)
}

_remove(root, value) {
if (root === null) {
return null
}

if (value < root.value) {
root.left = this._remove(root.left, value)
} else if (value > root.value) {
root.right = this._remove(root.right, value)
} else {
// Node to be removed found
if (root.left === null) {
return root.right
} else if (root.right === null) {
return root.left
}

// Node with two children, get the inorder successor (smallest in the right subtree)
root.value = this._minValueNode(root.right)

// Delete the inorder successor
root.right = this._remove(root.right, root.value)
}

return root
}

_minValueNode(node) {
let current = node
while (current.left !== null) {
current = current.left
}
return current.value
}

// Traverse

// in-order
inOrder(node, callback) {
if (node !== null) {
this.inOrder(node.left, callback)
callback(node.value)
this.inOrder(node.right, callback)
}
}

// pre-order
preOrder(node, callback) {
if (node !== null) {
callback(node.value)
this.preOrder(node.left, callback)
this.preOrder(node.right, callback)
}
}

// post-order
postOrder(node, callback) {
if (node !== null) {
this.postOrder(node.left, callback)
this.postOrder(node.right, callback)
callback(node.value)
}
}

// Traverse DFS
traverseDFS(callback, order = 'in-order') {
if (order === 'in-order') {
this.inOrder(this.root, callback)
} else if (order === 'pre-order') {
this.preOrder(this.root, callback)
} else if (order === 'post-order') {
this.postOrder(this.root, callback)
} else {
console.error('Invalid traversal order')
}
}

// Traverse BFS
traverseBFS(callback) {
if (!this.root) {
return
}

const queue = [this.root]

while (queue.length > 0) {
const node = queue.shift()
callback(node.value)

if (node.left) {
queue.push(node.left)
}
if (node.right) {
queue.push(node.right)
}
}
}

// Contains
contains(node = this.root, value) {
if (node === null) {
return false
}

if (value === node.value) {
return true
} else if (value < node.value) {
return this.contains(node.left, value)
} else {
return this.contains(node.right, value)
}
}
}

// Example usage:
const tree = new BinaryTree()
tree.insert(10)
tree.insert(5)
tree.insert(15)
tree.insert(3)
tree.insert(7)
tree.insert(66)

tree.remove(66)

tree.contains(5)

// Tree visualization:
// 10
// / \\
// 5 15
// / \\
// 3 7

console.log("In-order DFS traversal:")
tree.traverseDFS((value) => console.log(value), "in-order") // 3, 5, 7, 10, 15

console.log("\nPre-order DFS traversal:")
tree.traverseDFS((value) => console.log(value), "pre-order") // 10, 5, 3, 7, 15

console.log("\nPost-order DFS traversal:")
tree.traverseDFS((value) => console.log(value), "post-order") // 3, 7, 5, 15, 10

console.log("BFS Traversal:")
tree.traverseBFS((value) => console.log(value)) // 10, 5, 15, 3, 7

3. Graph

Consists of vertices and edges.

class Graph {
constructor() {
this.adjacencyList = {}
}

addVertex(vertex) {
if (!this.adjacencyList[vertex]) {
this.adjacencyList[vertex] = []
}
}

addEdge(vertex1, vertex2) {
if (!this.adjacencyList[vertex1] || !this.adjacencyList[vertex2]) {
console.error('Invalid vertex')
return
}

this.adjacencyList[vertex1].push(vertex2)
this.adjacencyList[vertex2].push(vertex1)
}

removeEdge(vertex1, vertex2) {
this.adjacencyList[vertex1] = this.adjacencyList[vertex1].filter(v => v !== vertex2)
this.adjacencyList[vertex2] = this.adjacencyList[vertex2].filter(v => v !== vertex1)
}

removeVertex(vertex) {
if (!this.adjacencyList[vertex]) {
console.error('Vertex does not exist')
return
}

while (this.adjacencyList[vertex].length) {
const adjacentVertex = this.adjacencyList[vertex].pop()
this.removeEdge(vertex, adjacentVertex)
}

delete this.adjacencyList[vertex]
}

traverseBFS(start) {
const queue = [start]
const result = []
const visited = {}
let currentVertex

visited[start] = true

while (queue.length) {
currentVertex = queue.shift()
result.push(currentVertex)

this.adjacencyList[currentVertex].forEach((neighbor) => {
if (!visited[neighbor]) {
visited[neighbor] = true
queue.push(neighbor)
}
})
}

return result
}

traverseDFS(start) {
const result = []
const visited = {}
const adjacencyList = this.adjacencyList

function dfs(vertex) {
if (!vertex) {
return null
}

visited[vertex] = true
result.push(vertex)

adjacencyList[vertex].forEach((neighbor) => {
if (!visited[neighbor]) {
return dfs(neighbor)
}
})
}

dfs(start)

return result
}

printGraph() {
for (const vertex in this.adjacencyList) {
const neighbors = this.adjacencyList[vertex]
console.log(`${vertex} -> ${neighbors.join(', ')}`)
}
}
}

// Example usage
const graph = new Graph()

graph.addVertex('A')
graph.addVertex('B')
graph.addVertex('C')
graph.addVertex('D')

graph.addEdge('A', 'B')
graph.addEdge('B', 'C')
graph.addEdge('C', 'D')
graph.addEdge('D', 'A')

/*
Graph Visualization:
A -------- B
| |
| |
D -------- C
*/

graph.printGraph()
// Output:
// A -> B, D
// B -> A, C
// C -> B, D
// D -> C, A

// Traverse
console.log("BFS:", graph.traverseBFS('A')) // Example BFS starting from vertex 'A'
// Output: ['A', 'B', 'D', 'C']
console.log("DFS:", graph.traverseDFS('A')) // Example DFS starting from vertex 'A'
// Output: ['A', 'B', 'C', 'D']

console.log('Before removal:', graph.adjacencyList)
graph.removeVertex('C')
console.log('After removing vertex C:', graph.adjacencyList)
graph.removeEdge('A', 'B')
console.log('After removing edge between A and B:', graph.adjacencyList)

graph.printGraph()
// Output:
// A -> D
// B ->
// D -> A

3.1. Adjacency Matrix

An adjacency matrix is a square matrix used to represent a graph. The elements of the matrix indicate whether pairs of vertices are adjacent or not in the graph. The size of the matrix is V x V, where V is the number of vertices in the graph.

   A  B  C  D
A 0 1 1 1
B 1 0 1 0
C 1 1 0 1
D 1 0 1 0
const adjacencyMatrix = [
[0, 1, 1, 1],
[1, 0, 1, 0],
[1, 1, 0, 1],
[1, 0, 1, 0]
]
function addValue(vertex1, vertex2) {
adjacencyMatrix[vertex1][vertex2] = 1
}

function removeValue(vertex1, vertex2) {
adjacencyMatrix[vertex1][vertex2] = 0
}

In this example:

  • Vertex A is adjacent to B, C, and D.
  • Vertex B is adjacent to A, C.
  • Vertex C is adjacent to A, B, D.
  • Vertex D is adjacent to A, C.

3.2. Adjacency List

An adjacency list is another way to represent a graph. In this representation, each vertex in the graph maintains a list of its neighboring vertices. This is especially useful for sparse graphs, where the number of edges is significantly less than the maximum possible number of edges.

A: B, C, D
B: A, C
C: A, B, D
D: A, C
const adjacencyList = {
A: ['B', 'C', 'D'],
B: ['A', 'C'],
C: ['A', 'B', 'D'],
D: ['A', 'C']
}

// or using Map
const adjacencyListMap = new Map([
['A', ['B', 'C', 'D']],
['B', ['A', 'C']],
['C', ['A', 'B', 'D']],
['D', ['A', 'C']]
])
function addValue(vertex1, vertex2) {
adjacencyList[vertex1].push(vertex2)
adjacencyList[vertex2].push(vertex1)
}

function removeValue(vertex1, vertex2) {
const vertex1Ind = adjacencyList[vertex1].indexOf(vertex2)
const vertex2Ind = adjacencyList[vertex2].indexOf(vertex1)

adjacencyList[vertex1].splice(vertex2Ind, 1)
adjacencyList[vertex2].splice(vertex1Ind, 1)
}

--

--