Skip to content

feat: Implement Levenshtein distance and BFS #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions algorithms/dp/levenshtein-distance/levenshteinDistance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {levenshteinDistance} from './levenshteinDistance';

describe('levenshteinDistance', () => {
test('identical strings should have a distance of 0', () => {
expect(levenshteinDistance('abc', 'abc')).toBe(0);
expect(levenshteinDistance('', '')).toBe(0);
expect(levenshteinDistance('hello', 'hello')).toBe(0);
});
test('empty string vs non-empty string should have distance equal to length', () => {
expect(levenshteinDistance('', 'abc')).toBe(3);
expect(levenshteinDistance('abc', '')).toBe(3);
});
test('simple single operation cases', () => {
expect(levenshteinDistance('abc', 'abcd')).toBe(1);
expect(levenshteinDistance('abc', 'ab')).toBe(1);
expect(levenshteinDistance('abc', 'abd')).toBe(1);
});
test('complex edit distance cases', () => {
expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
expect(levenshteinDistance('saturday', 'sunday')).toBe(3);
expect(levenshteinDistance('intention', 'execution')).toBe(5);
});
test('case sensitivity', () => {
expect(levenshteinDistance('abc', 'ABC')).toBe(3);
expect(levenshteinDistance('Hello', 'hello')).toBe(1);
});
});
34 changes: 34 additions & 0 deletions algorithms/dp/levenshtein-distance/levenshteinDistance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Calculates the Levenshtein distance (edit distance) between two strings.
* The edit distance is the minimum number of single-character operations
* (insertions, deletions, or substitutions) required to change one string into the other.
*
* @param {string} s1 - The first string.
* @param {string} s2 - The second string.
* @returns {number} The minimum number of operations required to transform s1 into s2.
*/
export function levenshteinDistance(s1: string, s2: string): number {
const m = s1.length;
const n = s2.length;
// Create a matrix of size (m+1) x (n+1)
const dp: number[][] = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
// Initialize the first column
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
// Initialize the first row
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
// Fill the matrix
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (s1[i - 1] === s2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1]; // No operation needed
} else {
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
}
}
}
return dp[m][n];
}
146 changes: 146 additions & 0 deletions algorithms/search/bfs/bfs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {bfs, reconstructPath} from './bfs';

describe('BFS Algorithm', () => {
test('should find the shortest paths in a simple graph', () => {
const graph = [
[1, 2], // Edges from vertex 0
[0, 3, 4], // Edges from vertex 1
[0, 5], // Edges from vertex 2
[1], // Edges from vertex 3
[1], // Edges from vertex 4
[2], // Edges from vertex 5
];
const {distances, predecessors} = bfs(graph, 0);
expect(distances).toEqual([0, 1, 1, 2, 2, 2]);
expect(predecessors).toEqual([-1, 0, 0, 1, 1, 2]);
});
test('should handle disconnected graph', () => {
const graph = [
[1], // Edges from vertex 0
[0], // Edges from vertex 1
[3], // Edges from vertex 2
[2], // Edges from vertex 3
[], // Edges from vertex 4 (isolated)
];
const {distances, predecessors} = bfs(graph, 0);
expect(distances).toEqual([0, 1, Infinity, Infinity, Infinity]);
expect(predecessors).toEqual([-1, 0, -1, -1, -1]);
expect(distances[2]).toBe(Infinity);
expect(distances[3]).toBe(Infinity);
expect(distances[4]).toBe(Infinity);
expect(predecessors[2]).toBe(-1);
expect(predecessors[3]).toBe(-1);
expect(predecessors[4]).toBe(-1);
});
test('should throw error for invalid start vertex', () => {
const graph = [[1, 2], [0], [0]];
expect(() => bfs(graph, -1)).toThrow('Start vertex is out of range');
expect(() => bfs(graph, 3)).toThrow('Start vertex is out of range');
});
test('should handle a graph with a single vertex', () => {
const graph = [[]];
const {distances, predecessors} = bfs(graph, 0);
expect(distances).toEqual([0]);
expect(predecessors).toEqual([-1]);
});
test('should handle BFS on a tree structure', () => {
const graph = [
[1, 2], // Root has two children
[3, 4], // Left child has two children
[5], // Right child has one child
[],
[],
[], // Leaf nodes
];
const {distances, predecessors} = bfs(graph, 0);
expect(distances).toEqual([0, 1, 1, 2, 2, 2]);
expect(predecessors).toEqual([-1, 0, 0, 1, 1, 2]);
});
test('should handle cyclic graphs correctly', () => {
// A graph with cycles: 0-1-2-0 and 3-4-5-3
const graph = [
[1, 2], // Edges from vertex 0
[0, 2], // Edges from vertex 1
[0, 1], // Edges from vertex 2
[4, 5], // Edges from vertex 3
[3, 5], // Edges from vertex 4
[3, 4], // Edges from vertex 5
];
const {distances} = bfs(graph, 0);
expect(distances[0]).toBe(0);
expect(distances[1]).toBe(1);
expect(distances[2]).toBe(1);
expect(distances[3]).toBe(Infinity);
expect(distances[4]).toBe(Infinity);
expect(distances[5]).toBe(Infinity);

const result2 = bfs(graph, 3);
expect(result2.distances[3]).toBe(0);
expect(result2.distances[4]).toBe(1);
expect(result2.distances[5]).toBe(1);
expect(result2.distances[0]).toBe(Infinity);
expect(result2.distances[1]).toBe(Infinity);
expect(result2.distances[2]).toBe(Infinity);
});

test('should handle large graphs efficiently', () => {
// Create a larger graph (path graph with 1000 vertices)
const largeGraph: number[][] = Array(1000)
.fill(0)
.map((_, i) => {
if (i === 0) return [1];
if (i === 999) return [998];
return [i - 1, i + 1];
});

const startTime = performance.now();
const {distances} = bfs(largeGraph, 0);
const endTime = performance.now();

expect(distances[0]).toBe(0);
expect(distances[1]).toBe(1);
expect(distances[10]).toBe(10);
expect(distances[100]).toBe(100);
expect(distances[999]).toBe(999);
expect(endTime - startTime).toBeLessThan(1000); // Should complete in less than 1 second
});
});

describe('reconstructPath', () => {
test('should reconstruct the correct path', () => {
const graph = [[1, 2], [3], [4], [], []];
const {predecessors} = bfs(graph, 0);
const path1 = reconstructPath(0, 3, predecessors);
expect(path1).toEqual([0, 1, 3]);

const path2 = reconstructPath(0, 4, predecessors);
expect(path2).toEqual([0, 2, 4]);
});

test('should handle path from vertex to itself', () => {
const graph = [[1], [2], []];
const {predecessors} = bfs(graph, 0);

const path = reconstructPath(0, 0, predecessors);
expect(path).toEqual([0]);
});

test('should return null for unreachable vertices', () => {
const graph = [[1], [0], [3], [2]];
const {predecessors, distances} = bfs(graph, 0);

expect(distances[2]).toBe(Infinity);
expect(distances[3]).toBe(Infinity);

const path = reconstructPath(0, 2, predecessors);
expect(path).toBeNull();
});

test('should handle invalid target vertex', () => {
const graph = [[1], [0]];
const {predecessors} = bfs(graph, 0);

const path = reconstructPath(0, 2, predecessors);
expect(path).toBeNull();
});
});
81 changes: 81 additions & 0 deletions algorithms/search/bfs/bfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Performs a breadth-first search (BFS) traversal on a graph.
* BFS explores all vertices at the current level before moving to vertices at the next level.
*
* @param {number[][]} graph - An adjacency list representation of the graph.
* @param {number} start - The starting vertex.
* @returns {{ distances: number[], predecessors: number[] }}
* An object containing distances from the start vertex to all other vertices,
* and predecessors array for path reconstruction.
* @throws {Error} If the start vertex is out of range.
*/
export function bfs(
graph: number[][],
start: number,
): {
distances: number[];
predecessors: number[];
} {
if (start < 0 || start >= graph.length) {
throw new Error('Start vertex is out of range');
}
const n = graph.length;
const distances: number[] = Array(n).fill(Infinity);
const predecessors: number[] = Array(n).fill(-1);
const visited: boolean[] = Array(n).fill(false);
// Initialize the queue with the start vertex
const queue: number[] = [start];
distances[start] = 0;
visited[start] = true;

while (queue.length > 0) {
const current = queue.shift()!;
// Visit all adjacent vertices
for (const neighbor of graph[current]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
distances[neighbor] = distances[current] + 1;
predecessors[neighbor] = current;
queue.push(neighbor);
}
}
}

return {distances, predecessors};
}

/**
* Reconstructs the shortest path from a source vertex to a target vertex
* using the predecessors array obtained from BFS.
*
* @param {number} source - The source vertex.
* @param {number} target - The target vertex.
* @param {number[]} predecessors - The predecessors array from BFS.
* @returns {number[] | null} The path from source to target, or null if no path exists.
*/
export function reconstructPath(
source: number,
target: number,
predecessors: number[],
): number[] | null {
if (
target < 0 ||
target >= predecessors.length ||
(predecessors[target] === -1 && source !== target)
) {
return null;
}

const path: number[] = [];
let current = target;

while (current !== -1) {
path.unshift(current);
if (current === source) {
break;
}
current = predecessors[current];
}
// Check if the path starts with the source vertex
return path[0] === source ? path : null;
}