From d79b162b306a378e26af10c7168c353c0a4596e8 Mon Sep 17 00:00:00 2001 From: monkey0722 Date: Thu, 17 Apr 2025 22:44:01 +0900 Subject: [PATCH 1/2] feat: add Levenshtein distance algorithm with tests --- .../levenshteinDistance.test.ts | 27 +++++++++++++++ .../levenshteinDistance.ts | 34 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 algorithms/dp/levenshtein-distance/levenshteinDistance.test.ts create mode 100644 algorithms/dp/levenshtein-distance/levenshteinDistance.ts diff --git a/algorithms/dp/levenshtein-distance/levenshteinDistance.test.ts b/algorithms/dp/levenshtein-distance/levenshteinDistance.test.ts new file mode 100644 index 0000000..2f47687 --- /dev/null +++ b/algorithms/dp/levenshtein-distance/levenshteinDistance.test.ts @@ -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); + }); +}); diff --git a/algorithms/dp/levenshtein-distance/levenshteinDistance.ts b/algorithms/dp/levenshtein-distance/levenshteinDistance.ts new file mode 100644 index 0000000..ff213d1 --- /dev/null +++ b/algorithms/dp/levenshtein-distance/levenshteinDistance.ts @@ -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]; +} From 057962caa1642e7585055cd6960c05f347416abe Mon Sep 17 00:00:00 2001 From: monkey0722 Date: Thu, 17 Apr 2025 22:50:53 +0900 Subject: [PATCH 2/2] feat: implement BFS algorithm with path reconstruction and tests --- algorithms/search/bfs/bfs.test.ts | 146 ++++++++++++++++++++++++++++++ algorithms/search/bfs/bfs.ts | 81 +++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 algorithms/search/bfs/bfs.test.ts create mode 100644 algorithms/search/bfs/bfs.ts diff --git a/algorithms/search/bfs/bfs.test.ts b/algorithms/search/bfs/bfs.test.ts new file mode 100644 index 0000000..e4261a9 --- /dev/null +++ b/algorithms/search/bfs/bfs.test.ts @@ -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(); + }); +}); diff --git a/algorithms/search/bfs/bfs.ts b/algorithms/search/bfs/bfs.ts new file mode 100644 index 0000000..23af260 --- /dev/null +++ b/algorithms/search/bfs/bfs.ts @@ -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; +}