Top Algorithms Implemented in JavaScript

Avatar

By squashlabs, Last Updated: September 25, 2023

Top Algorithms Implemented in JavaScript

Introduction to JavaScript Algorithms

Algorithms are a key aspect of software development. In this article, we will explore the fundamentals of algorithms implemented in JavaScript and their importance in building robust and optimized applications.

Related Article: 25 Handy Javascript Code Snippets for Everyday Use

Example: Finding the Maximum Number in an Array

To illustrate the concept, let’s consider a simple algorithm that finds the maximum number in an array. Here’s an implementation in JavaScript:

function findMax(arr) {
  let max = arr[0];
  for (let i = 1; i  max) {
      max = arr[i];
    }
  }
  return max;
}

const numbers = [5, 8, 3, 10, 2];
console.log(findMax(numbers)); // Output: 10

In this example, we iterate through the array and update the max variable whenever we encounter a larger number. Finally, we return the maximum value found.

Example: Checking if a Number is Prime

Another common algorithm is checking whether a given number is prime. Here’s how it can be implemented in JavaScript:

function isPrime(num) {
  if (num <= 1) {
    return false;
  }
  for (let i = 2; i < num; i++) {
    if (num % i === 0) {
      return false;
    }
  }
  return true;
}

console.log(isPrime(17)); // Output: true
console.log(isPrime(25)); // Output: false

In this example, we iterate from 2 to the given number and check if any number divides it evenly. If we find such a number, the given number is not prime. Otherwise, it is prime.

Implementation of the Most Important Algorithms

In this chapter, we will dive into the implementation of some of the most important algorithms used in JavaScript. These algorithms form the foundation of various problem-solving techniques and are essential for any JavaScript developer.

Sorting Algorithms in JavaScript

Sorting algorithms are used to arrange elements in a specific order, such as ascending or descending. JavaScript provides several sorting algorithms that can be employed based on the requirements of the application.

Bubble Sort

Bubble sort is a simple comparison-based sorting algorithm. It repeatedly steps through the list, compares adjacent elements, and swaps them if they are in the wrong order. Here’s an example implementation in JavaScript:

function bubbleSort(arr) {
  const len = arr.length;
  for (let i = 0; i < len - 1; i++) {
    for (let j = 0; j  arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

const numbers = [5, 2, 8, 1, 4];
console.log(bubbleSort(numbers)); // Output: [1, 2, 4, 5, 8]

In this example, we iterate through the array multiple times, comparing adjacent elements and swapping them if they are in the wrong order. This process continues until the entire array is sorted.

Quick Sort

Quick sort is a widely used sorting algorithm that follows the divide-and-conquer approach. It partitions the array into two sub-arrays, then recursively sorts each sub-array. Here’s an example implementation in JavaScript:

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = [];
  const right = [];
  for (let i = 0; i < arr.length; i++) {
    if (i === Math.floor(arr.length / 2)) {
      continue;
    }
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  return [...quickSort(left), pivot, ...quickSort(right)];
}

const numbers = [5, 2, 8, 1, 4];
console.log(quickSort(numbers)); // Output: [1, 2, 4, 5, 8]

In this example, we select a pivot element from the array and partition it into two sub-arrays: one with elements smaller than the pivot and another with elements greater than the pivot. We then recursively apply the same process to these sub-arrays until the entire array is sorted.

String Algorithms in JavaScript

String algorithms involve manipulating and processing strings efficiently. JavaScript provides various built-in methods and algorithms to handle string operations effectively.

Reverse a String

Reversing a string is a common string algorithm. Here’s an example implementation in JavaScript:

function reverseString(str) {
  return str.split('').reverse().join('');
}

const text = 'Hello, World!';
console.log(reverseString(text)); // Output: '!dlroW ,olleH'

In this example, we split the string into an array of characters, reverse the array, and then join the characters back into a string to obtain the reversed version of the original string.

Check if Two Strings are Anagrams

An anagram is a word or phrase formed by rearranging the letters of another word or phrase. Here’s an example implementation in JavaScript to check if two strings are anagrams:

function areAnagrams(str1, str2) {
  const sortedStr1 = str1.split('').sort().join('');
  const sortedStr2 = str2.split('').sort().join('');
  return sortedStr1 === sortedStr2;
}

console.log(areAnagrams('listen', 'silent')); // Output: true
console.log(areAnagrams('hello', 'world')); // Output: false

In this example, we split both strings into arrays of characters, sort the arrays, and then join them back into strings. Finally, we compare the sorted strings to check if they are equal. If they are equal, the two strings are anagrams.

Divide and Conquer Algorithms in JavaScript

Divide and conquer algorithms solve a problem by breaking it down into smaller, more manageable parts, solving each part individually, and then combining the solutions to form the final result. JavaScript provides useful techniques for implementing these algorithms effectively.

Binary search is an efficient divide and conquer algorithm used to find a specific element in a sorted array. Here’s an example implementation in JavaScript:

function binarySearch(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (arr[mid] === target) {
      return mid;
    } else if (arr[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }
  return -1; // Element not found
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
console.log(binarySearch(numbers, 6)); // Output: 5 (index of 6 in the array)
console.log(binarySearch(numbers, 11)); // Output: -1 (element not found)

In this example, we repeatedly divide the array into two halves and check if the target element is equal to the middle element. If it is, we return the index of the middle element. If the target element is less than the middle element, we continue the search in the left half of the array. Otherwise, we continue the search in the right half of the array. The process continues until the target element is found or the array is exhausted.

Example: Merge Sort

Merge sort is another popular divide and conquer algorithm that sorts an array by dividing it into two halves, sorting each half recursively, and then merging the sorted halves back together. Here’s an example implementation in JavaScript:

function mergeSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
  const mid = Math.floor(arr.length / 2);
  const left = mergeSort(arr.slice(0, mid));
  const right = mergeSort(arr.slice(mid));
  return merge(left, right);
}

function merge(left, right) {
  const merged = [];
  let leftIndex = 0;
  let rightIndex = 0;
  while (leftIndex < left.length && rightIndex < right.length) {
    if (left[leftIndex] < right[rightIndex]) {
      merged.push(left[leftIndex]);
      leftIndex++;
    } else {
      merged.push(right[rightIndex]);
      rightIndex++;
    }
  }
  return merged.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

const numbers = [5, 2, 8, 1, 4];
console.log(mergeSort(numbers)); // Output: [1, 2, 4, 5, 8]

In this example, we recursively divide the array into halves until each sub-array contains only one element. Then, we merge the sorted sub-arrays by comparing the elements from each sub-array, picking the smaller element, and appending it to the merged array. The process continues until the entire array is sorted.

Recursion and Backtracking Algorithms in JavaScript

Recursion and backtracking algorithms are useful techniques for solving problems that involve repetitive sub-problems or exploring multiple possible solutions. JavaScript provides the necessary tools to implement these algorithms effectively.

Example: Factorial Calculation

Calculating the factorial of a number is a classic example of a recursive algorithm. Here’s an example implementation in JavaScript:

function factorial(num) {
  if (num === 0 || num === 1) {
    return 1;
  }
  return num * factorial(num - 1);
}

console.log(factorial(5)); // Output: 120 (5! = 5 * 4 * 3 * 2 * 1)

In this example, we define the factorial function that recursively calls itself with a smaller number until it reaches the base case (num = 0 or num = 1). The base case returns 1, and each recursive call multiplies the current number with the factorial of the previous number.

Example: N-Queens Problem

The N-Queens problem is a classic backtracking problem that aims to place N queens on an N×N chessboard in such a way that no two queens threaten each other. Here’s an example implementation in JavaScript:

function solveNQueens(n) {
  const board = new Array(n);
  for (let i = 0; i  row.join('')));
    return;
  }
  for (let col = 0; col < board.length; col++) {
    if (isValid(board, row, col)) {
      board[row][col] = 'Q';
      backtrack(board, row + 1, result);
      board[row][col] = '.';
    }
  }
}

function isValid(board, row, col) {
  for (let i = 0; i = 0 && j >= 0; i--, j--) {
    if (board[i][j] === 'Q') {
      return false;
    }
  }
  for (let i = row - 1, j = col + 1; i >= 0 && j < board.length; i--, j++) {
    if (board[i][j] === 'Q') {
      return false;
    }
  }
  return true;
}

console.log(solveNQueens(4));
// Output: [
//   [".Q..", "...Q", "Q...", "..Q."],
//   ["..Q.", "Q...", "...Q", ".Q.."]
// ]

In this example, we define the solveNQueens function that initializes an empty chessboard and calls the backtrack function to explore all possible configurations. The backtrack function uses recursion to place queens on the board row by row, checking if each placement is valid. If a valid configuration is found, it is added to the result array. The isValid function checks if a queen placement is valid by examining the rows, columns, and diagonals.

Greedy Algorithms in JavaScript

Greedy algorithms follow a greedy strategy that makes locally optimal choices at each step, with the hope that these choices will lead to a globally optimal solution. JavaScript provides tools to implement greedy algorithms efficiently.

Example: Fractional Knapsack Problem

The fractional knapsack problem involves selecting items with maximum total value while not exceeding a given weight constraint. Here’s an example implementation of the fractional knapsack algorithm in JavaScript:

function fractionalKnapsack(items, capacity) {
  items.sort((a, b) => b.value / b.weight - a.value / a.weight);
  let totalValue = 0;
  let remainingCapacity = capacity;
  for (const item of items) {
    if (remainingCapacity >= item.weight) {
      totalValue += item.value;
      remainingCapacity -= item.weight;
    } else {
      totalValue += remainingCapacity * (item.value / item.weight);
      break;
    }
  }
  return totalValue;
}

const knapsackItems = [
  { value: 60, weight: 10 },
  { value: 100, weight: 20 },
  { value: 120, weight: 30 }
];
const knapsackCapacity = 50;
console.log(fractionalKnapsack(knapsackItems, knapsackCapacity)); // Output: 240

In this example, we sort the items based on their value-to-weight ratio in descending order. Then, we iteratively select items until the knapsack’s capacity is exhausted. If an item can be fully included, we add its value to the total value and reduce the remaining capacity. If an item cannot be fully included, we calculate the fractional value based on the remaining capacity and add it to the total value.

Example: Interval Scheduling Problem

The interval scheduling problem involves selecting the maximum number of non-overlapping intervals from a given set. Here’s an example implementation of the interval scheduling algorithm (also known as the activity selection problem) in JavaScript:

function intervalScheduling(intervals) {
  intervals.sort((a, b) => a.end - b.end);
  const selectedIntervals = [intervals[0]];
  let lastSelected = 0;
  for (let i = 1; i = intervals[lastSelected].end) {
      selectedIntervals.push(intervals[i]);
      lastSelected = i;
    }
  }
  return selectedIntervals;
}

const intervals = [
  { start: 1, end: 4 },
  { start: 3, end: 5 },
  { start: 0, end: 6 },
  { start: 5, end: 7 },
  { start: 3, end: 9 },
  { start: 5, end: 9 },
  { start: 6, end: 10 },
  { start: 8, end: 11 },
  { start: 8, end: 12 },
  { start: 2, end: 14 },
  { start: 12, end: 16 }
];
console.log(intervalScheduling(intervals));
// Output: [
//   { start: 1, end: 4 },
//   { start: 5, end: 7 },
//   { start: 8, end: 11 },
//   { start: 12, end: 16 }
// ]

In this example, we sort the intervals based on their end times in ascending order. We initialize an array with the first interval as the selected interval. Then, we iterate through the remaining intervals, selecting an interval if its start time is greater than or equal to the end time of the last selected interval. This ensures that the selected intervals do not overlap.

Dynamic Programming Algorithms in JavaScript

Dynamic programming algorithms solve complex problems by breaking them down into simpler overlapping sub-problems and storing the results of these sub-problems to avoid redundant calculations. JavaScript provides techniques for implementing dynamic programming algorithms effectively.

Example: Fibonacci Sequence

The Fibonacci sequence is a classic example of a dynamic programming problem. It involves finding the value of a number in the sequence by summing the two preceding numbers. Here’s an example implementation in JavaScript:

function fibonacci(n) {
  const fib = [0, 1];
  for (let i = 2; i <= n; i++) {
    fib[i] = fib[i - 1] + fib[i - 2];
  }
  return fib[n];
}

console.log(fibonacci(6)); // Output: 8 (0, 1, 1, 2, 3, 5, 8)

In this example, we initialize an array with the first two numbers in the Fibonacci sequence. Then, we iteratively calculate the subsequent numbers by summing the two preceding numbers. The final element in the array represents the value of the number in the sequence at index n.

Example: Longest Increasing Subsequence

The longest increasing subsequence problem involves finding the length of the longest subsequence of a given sequence that is strictly increasing. Here’s an example implementation of the dynamic programming algorithm for finding the longest increasing subsequence length in JavaScript:

function longestIncreasingSubsequenceLength(nums) {
  const dp = new Array(nums.length).fill(1);
  let maxLength = 1;
  for (let i = 1; i < nums.length; i++) {
    for (let j = 0; j <i> nums[j]) {
        dp[i] = Math.max(dp[i], dp[j] + 1);
        maxLength = Math.max(maxLength, dp[i]);
      }
    }
  }
  return maxLength;
}

const sequence = [10, 22, 9, 33, 21, 50, 41, 60];
console.log(longestIncreasingSubsequenceLength(sequence)); // Output: 5

In this example, we initialize an array dp with all elements set to 1, as each number itself forms a subsequence of length 1. Then, we iterate through the numbers and compare each number with all previous numbers. If a number is greater than the previous number, we update the length of the subsequence ending at the current number by adding 1 to the length of the subsequence ending at the previous number. We also update the maximum length encountered so far. The final result is the maximum length of any subsequence.

Tree-related algorithms focus on solving problems related to tree data structures, such as tree traversal, finding common ancestors, or determining the height of a tree. JavaScript provides tools to implement tree-related algorithms efficiently.

Example: Binary Tree Traversal

Binary tree traversal involves visiting each node in a binary tree in a specific order. There are three common traversal techniques: pre-order, in-order, and post-order. Here’s an example implementation of these traversal techniques in JavaScript:

class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

function preOrderTraversal(node) {
  if (node === null) {
    return;
  }
  console.log(node.value);
  preOrderTraversal(node.left);
  preOrderTraversal(node.right);
}

function inOrderTraversal(node) {
  if (node === null) {
    return;
  }
  inOrderTraversal(node.left);
  console.log(node.value);
  inOrderTraversal(node.right);
}

function postOrderTraversal(node) {
  if (node === null) {
    return;
  }
  postOrderTraversal(node.left);
  postOrderTraversal(node.right);
  console.log(node.value);
}

// Create a binary tree
const root = new Node(1);
root.left = new Node(2);
root.right = new Node(3);
root.left.left = new Node(4);
root.left.right = new Node(5);

// Perform tree traversals
console.log("Pre-order traversal:");
preOrderTraversal(root);

console.log("In-order traversal:");
inOrderTraversal(root);

console.log("Post-order traversal:");
postOrderTraversal(root);

In this example, we define a Node class representing a node in a binary tree. We then define three recursive functions for pre-order, in-order, and post-order traversal. The pre-order traversal visits the root node first, followed by the left subtree and then the right subtree. The in-order traversal visits the left subtree, then the root node, and finally the right subtree. The post-order traversal visits the left subtree, the right subtree, and finally the root node.

Example: Lowest Common Ancestor

The lowest common ancestor (LCA) is the shared ancestor of two nodes in a tree that is located farthest from the root. Here’s an example implementation of the LCA algorithm in JavaScript:

class Node {
  constructor(value) {
    this.value = value;
    this.left = null;
    this.right = null;
  }
}

function findLowestCommonAncestor(root, p, q) {
  if (root === null || root === p || root === q) {
    return root;
  }
  const left = findLowestCommonAncestor(root.left, p, q);
  const right = findLowestCommonAncestor(root.right, p, q);
  if (left !== null && right !== null) {
    return root;
  } else if (left !== null) {
    return left;
  } else {
    return right;
  }
}

// Create a binary tree
const root = new Node(3);
root.left = new Node(5);
root.right = new Node(1);
root.left.left = new Node(6);
root.left.right = new Node(2);
root.right.left = new Node(0);
root.right.right = new Node(8);
root.left.right.left = new Node(7);
root.left.right.right = new Node(4);

const p = root.left;
const q = root.right;

const lca = findLowestCommonAncestor(root, p, q);
console.log(lca.value); // Output: 3

In this example, we define a Node class representing a node in a binary tree. We then define a recursive function findLowestCommonAncestor that traverses the tree to find the lowest common ancestor of two given nodes p and q. The function returns null if the current node is null or one of the nodes p or q. If both the left and right subtrees return non-null values, the current node is the lowest common ancestor. Otherwise, we return the non-null subtree value or null if both subtrees return null.

Graph Algorithms in JavaScript

Graph algorithms focus on solving problems related to graph data structures, such as finding the shortest path, detecting cycles, or performing topological sorting. JavaScript provides tools to implement graph algorithms efficiently.

Example: Breadth-First Search (BFS)

Breadth-first search (BFS) is used to traverse or search a graph or tree in a breadthward motion. Here’s an example implementation of breadth-first search in JavaScript:

class Graph {
  constructor() {
    this.adjacencyList = new Map();
  }

  addVertex(vertex) {
    if (!this.adjacencyList.has(vertex)) {
      this.adjacencyList.set(vertex, []);
    }
  }

  addEdge(v1, v2) {
    this.adjacencyList.get(v1).push(v2);
    this.adjacencyList.get(v2).push(v1);
  }

  breadthFirstSearch(startingVertex) {
    const visited = new Set();
    const queue = [startingVertex];
    visited.add(startingVertex);
    while (queue.length > 0) {
      const currentVertex = queue.shift();
      console.log(currentVertex);
      const neighbors = this.adjacencyList.get(currentVertex);
      for (const neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          queue.push(neighbor);
          visited.add(neighbor);
        }
      }
    }
  }
}

// Create a graph
const graph = new Graph();
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');
graph.addVertex('F');
graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('B', 'D');
graph.addEdge('C', 'E');
graph.addEdge('D', 'E');
graph.addEdge('D', 'F');
graph.addEdge('E', 'F');

// Perform breadth-first search
graph.breadthFirstSearch('A');

In this example, we define a Graph class with methods to add vertices and edges, as well as to perform breadth-first search. The addVertex method adds a vertex to the graph, and the addEdge method adds an edge between two vertices. The breadthFirstSearch method performs breadth-first search starting from a specified vertex, using a queue to keep track of the vertices to visit. The visited set ensures that each vertex is visited only once.

Example: Dijkstra’s Algorithm

Dijkstra’s algorithm is used to find the shortest path between two vertices in a graph with non-negative edge weights. Here’s an example implementation of Dijkstra’s algorithm in JavaScript:

class Graph {
  constructor() {
    this.vertices = new Set();
    this.edges = new Map();
  }

  addVertex(vertex) {
    this.vertices.add(vertex);
    this.edges.set(vertex, new Map());
  }

  addEdge(v1, v2, weight) {
    this.edges.get(v1).set(v2, weight);
    this.edges.get(v2).set(v1, weight);
  }

  dijkstra(startVertex) {
    const distances = new Map();
    const previous = new Map();
    const priorityQueue = new PriorityQueue();
    for (const vertex of this.vertices) {
      if (vertex === startVertex) {
        distances.set(vertex, 0);
        priorityQueue.enqueue(vertex, 0);
      } else {
        distances.set(vertex, Infinity);
        priorityQueue.enqueue(vertex, Infinity);
      }
      previous.set(vertex, null);
    }
    while (!priorityQueue.isEmpty()) {
      const currentVertex = priorityQueue.dequeue();
      const neighbors = this.edges.get(currentVertex);
      for (const [neighbor, weight] of neighbors) {
        const distance = distances.get(currentVertex) + weight;
        if (distance  a.priority - b.priority);
  }

  dequeue() {
    return this.queue.shift().item;
  }

  isEmpty() {
    return this.queue.length === 0;
  }
}

// Create a graph
const graph = new Graph();
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');
graph.addEdge('A', 'B', 4);
graph.addEdge('A', 'C', 2);
graph.addEdge('B', 'E', 3);
graph.addEdge('C', 'D', 2);
graph.addEdge('C', 'E', 1);
graph.addEdge('D', 'E', 3);

// Find the shortest path using Dijkstra's algorithm
const { distances, previous } = graph.dijkstra('A');
const shortestPath = graph.getShortestPathTo('E', previous);
console.log(shortestPath); // Output: ['A', 'C', 'E']

In this example, we define a Graph class with methods to add vertices and edges, as well as to perform Dijkstra’s algorithm. The addVertex method adds a vertex to the graph, and the addEdge method adds an edge between two vertices with a specified weight. The dijkstra method finds the shortest path from a specified start vertex to all other vertices, returning the distances and previous vertices. The getShortestPathTo method reconstructs the shortest path to a specified destination vertex using the previous vertices. The PriorityQueue class is used to efficiently dequeue vertices with the smallest priority (shortest distance).