Skip to content

Commit 8512404

Browse files
authored
Merge pull request #63 from monkey0722/new/algorithms
feat: Implement Levenshtein distance and BFS
2 parents f370635 + 057962c commit 8512404

File tree

4 files changed

+288
-0
lines changed

4 files changed

+288
-0
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import {levenshteinDistance} from './levenshteinDistance';
2+
3+
describe('levenshteinDistance', () => {
4+
test('identical strings should have a distance of 0', () => {
5+
expect(levenshteinDistance('abc', 'abc')).toBe(0);
6+
expect(levenshteinDistance('', '')).toBe(0);
7+
expect(levenshteinDistance('hello', 'hello')).toBe(0);
8+
});
9+
test('empty string vs non-empty string should have distance equal to length', () => {
10+
expect(levenshteinDistance('', 'abc')).toBe(3);
11+
expect(levenshteinDistance('abc', '')).toBe(3);
12+
});
13+
test('simple single operation cases', () => {
14+
expect(levenshteinDistance('abc', 'abcd')).toBe(1);
15+
expect(levenshteinDistance('abc', 'ab')).toBe(1);
16+
expect(levenshteinDistance('abc', 'abd')).toBe(1);
17+
});
18+
test('complex edit distance cases', () => {
19+
expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
20+
expect(levenshteinDistance('saturday', 'sunday')).toBe(3);
21+
expect(levenshteinDistance('intention', 'execution')).toBe(5);
22+
});
23+
test('case sensitivity', () => {
24+
expect(levenshteinDistance('abc', 'ABC')).toBe(3);
25+
expect(levenshteinDistance('Hello', 'hello')).toBe(1);
26+
});
27+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Calculates the Levenshtein distance (edit distance) between two strings.
3+
* The edit distance is the minimum number of single-character operations
4+
* (insertions, deletions, or substitutions) required to change one string into the other.
5+
*
6+
* @param {string} s1 - The first string.
7+
* @param {string} s2 - The second string.
8+
* @returns {number} The minimum number of operations required to transform s1 into s2.
9+
*/
10+
export function levenshteinDistance(s1: string, s2: string): number {
11+
const m = s1.length;
12+
const n = s2.length;
13+
// Create a matrix of size (m+1) x (n+1)
14+
const dp: number[][] = Array.from({length: m + 1}, () => Array(n + 1).fill(0));
15+
// Initialize the first column
16+
for (let i = 0; i <= m; i++) {
17+
dp[i][0] = i;
18+
}
19+
// Initialize the first row
20+
for (let j = 0; j <= n; j++) {
21+
dp[0][j] = j;
22+
}
23+
// Fill the matrix
24+
for (let i = 1; i <= m; i++) {
25+
for (let j = 1; j <= n; j++) {
26+
if (s1[i - 1] === s2[j - 1]) {
27+
dp[i][j] = dp[i - 1][j - 1]; // No operation needed
28+
} else {
29+
dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
30+
}
31+
}
32+
}
33+
return dp[m][n];
34+
}

algorithms/search/bfs/bfs.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import {bfs, reconstructPath} from './bfs';
2+
3+
describe('BFS Algorithm', () => {
4+
test('should find the shortest paths in a simple graph', () => {
5+
const graph = [
6+
[1, 2], // Edges from vertex 0
7+
[0, 3, 4], // Edges from vertex 1
8+
[0, 5], // Edges from vertex 2
9+
[1], // Edges from vertex 3
10+
[1], // Edges from vertex 4
11+
[2], // Edges from vertex 5
12+
];
13+
const {distances, predecessors} = bfs(graph, 0);
14+
expect(distances).toEqual([0, 1, 1, 2, 2, 2]);
15+
expect(predecessors).toEqual([-1, 0, 0, 1, 1, 2]);
16+
});
17+
test('should handle disconnected graph', () => {
18+
const graph = [
19+
[1], // Edges from vertex 0
20+
[0], // Edges from vertex 1
21+
[3], // Edges from vertex 2
22+
[2], // Edges from vertex 3
23+
[], // Edges from vertex 4 (isolated)
24+
];
25+
const {distances, predecessors} = bfs(graph, 0);
26+
expect(distances).toEqual([0, 1, Infinity, Infinity, Infinity]);
27+
expect(predecessors).toEqual([-1, 0, -1, -1, -1]);
28+
expect(distances[2]).toBe(Infinity);
29+
expect(distances[3]).toBe(Infinity);
30+
expect(distances[4]).toBe(Infinity);
31+
expect(predecessors[2]).toBe(-1);
32+
expect(predecessors[3]).toBe(-1);
33+
expect(predecessors[4]).toBe(-1);
34+
});
35+
test('should throw error for invalid start vertex', () => {
36+
const graph = [[1, 2], [0], [0]];
37+
expect(() => bfs(graph, -1)).toThrow('Start vertex is out of range');
38+
expect(() => bfs(graph, 3)).toThrow('Start vertex is out of range');
39+
});
40+
test('should handle a graph with a single vertex', () => {
41+
const graph = [[]];
42+
const {distances, predecessors} = bfs(graph, 0);
43+
expect(distances).toEqual([0]);
44+
expect(predecessors).toEqual([-1]);
45+
});
46+
test('should handle BFS on a tree structure', () => {
47+
const graph = [
48+
[1, 2], // Root has two children
49+
[3, 4], // Left child has two children
50+
[5], // Right child has one child
51+
[],
52+
[],
53+
[], // Leaf nodes
54+
];
55+
const {distances, predecessors} = bfs(graph, 0);
56+
expect(distances).toEqual([0, 1, 1, 2, 2, 2]);
57+
expect(predecessors).toEqual([-1, 0, 0, 1, 1, 2]);
58+
});
59+
test('should handle cyclic graphs correctly', () => {
60+
// A graph with cycles: 0-1-2-0 and 3-4-5-3
61+
const graph = [
62+
[1, 2], // Edges from vertex 0
63+
[0, 2], // Edges from vertex 1
64+
[0, 1], // Edges from vertex 2
65+
[4, 5], // Edges from vertex 3
66+
[3, 5], // Edges from vertex 4
67+
[3, 4], // Edges from vertex 5
68+
];
69+
const {distances} = bfs(graph, 0);
70+
expect(distances[0]).toBe(0);
71+
expect(distances[1]).toBe(1);
72+
expect(distances[2]).toBe(1);
73+
expect(distances[3]).toBe(Infinity);
74+
expect(distances[4]).toBe(Infinity);
75+
expect(distances[5]).toBe(Infinity);
76+
77+
const result2 = bfs(graph, 3);
78+
expect(result2.distances[3]).toBe(0);
79+
expect(result2.distances[4]).toBe(1);
80+
expect(result2.distances[5]).toBe(1);
81+
expect(result2.distances[0]).toBe(Infinity);
82+
expect(result2.distances[1]).toBe(Infinity);
83+
expect(result2.distances[2]).toBe(Infinity);
84+
});
85+
86+
test('should handle large graphs efficiently', () => {
87+
// Create a larger graph (path graph with 1000 vertices)
88+
const largeGraph: number[][] = Array(1000)
89+
.fill(0)
90+
.map((_, i) => {
91+
if (i === 0) return [1];
92+
if (i === 999) return [998];
93+
return [i - 1, i + 1];
94+
});
95+
96+
const startTime = performance.now();
97+
const {distances} = bfs(largeGraph, 0);
98+
const endTime = performance.now();
99+
100+
expect(distances[0]).toBe(0);
101+
expect(distances[1]).toBe(1);
102+
expect(distances[10]).toBe(10);
103+
expect(distances[100]).toBe(100);
104+
expect(distances[999]).toBe(999);
105+
expect(endTime - startTime).toBeLessThan(1000); // Should complete in less than 1 second
106+
});
107+
});
108+
109+
describe('reconstructPath', () => {
110+
test('should reconstruct the correct path', () => {
111+
const graph = [[1, 2], [3], [4], [], []];
112+
const {predecessors} = bfs(graph, 0);
113+
const path1 = reconstructPath(0, 3, predecessors);
114+
expect(path1).toEqual([0, 1, 3]);
115+
116+
const path2 = reconstructPath(0, 4, predecessors);
117+
expect(path2).toEqual([0, 2, 4]);
118+
});
119+
120+
test('should handle path from vertex to itself', () => {
121+
const graph = [[1], [2], []];
122+
const {predecessors} = bfs(graph, 0);
123+
124+
const path = reconstructPath(0, 0, predecessors);
125+
expect(path).toEqual([0]);
126+
});
127+
128+
test('should return null for unreachable vertices', () => {
129+
const graph = [[1], [0], [3], [2]];
130+
const {predecessors, distances} = bfs(graph, 0);
131+
132+
expect(distances[2]).toBe(Infinity);
133+
expect(distances[3]).toBe(Infinity);
134+
135+
const path = reconstructPath(0, 2, predecessors);
136+
expect(path).toBeNull();
137+
});
138+
139+
test('should handle invalid target vertex', () => {
140+
const graph = [[1], [0]];
141+
const {predecessors} = bfs(graph, 0);
142+
143+
const path = reconstructPath(0, 2, predecessors);
144+
expect(path).toBeNull();
145+
});
146+
});

algorithms/search/bfs/bfs.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Performs a breadth-first search (BFS) traversal on a graph.
3+
* BFS explores all vertices at the current level before moving to vertices at the next level.
4+
*
5+
* @param {number[][]} graph - An adjacency list representation of the graph.
6+
* @param {number} start - The starting vertex.
7+
* @returns {{ distances: number[], predecessors: number[] }}
8+
* An object containing distances from the start vertex to all other vertices,
9+
* and predecessors array for path reconstruction.
10+
* @throws {Error} If the start vertex is out of range.
11+
*/
12+
export function bfs(
13+
graph: number[][],
14+
start: number,
15+
): {
16+
distances: number[];
17+
predecessors: number[];
18+
} {
19+
if (start < 0 || start >= graph.length) {
20+
throw new Error('Start vertex is out of range');
21+
}
22+
const n = graph.length;
23+
const distances: number[] = Array(n).fill(Infinity);
24+
const predecessors: number[] = Array(n).fill(-1);
25+
const visited: boolean[] = Array(n).fill(false);
26+
// Initialize the queue with the start vertex
27+
const queue: number[] = [start];
28+
distances[start] = 0;
29+
visited[start] = true;
30+
31+
while (queue.length > 0) {
32+
const current = queue.shift()!;
33+
// Visit all adjacent vertices
34+
for (const neighbor of graph[current]) {
35+
if (!visited[neighbor]) {
36+
visited[neighbor] = true;
37+
distances[neighbor] = distances[current] + 1;
38+
predecessors[neighbor] = current;
39+
queue.push(neighbor);
40+
}
41+
}
42+
}
43+
44+
return {distances, predecessors};
45+
}
46+
47+
/**
48+
* Reconstructs the shortest path from a source vertex to a target vertex
49+
* using the predecessors array obtained from BFS.
50+
*
51+
* @param {number} source - The source vertex.
52+
* @param {number} target - The target vertex.
53+
* @param {number[]} predecessors - The predecessors array from BFS.
54+
* @returns {number[] | null} The path from source to target, or null if no path exists.
55+
*/
56+
export function reconstructPath(
57+
source: number,
58+
target: number,
59+
predecessors: number[],
60+
): number[] | null {
61+
if (
62+
target < 0 ||
63+
target >= predecessors.length ||
64+
(predecessors[target] === -1 && source !== target)
65+
) {
66+
return null;
67+
}
68+
69+
const path: number[] = [];
70+
let current = target;
71+
72+
while (current !== -1) {
73+
path.unshift(current);
74+
if (current === source) {
75+
break;
76+
}
77+
current = predecessors[current];
78+
}
79+
// Check if the path starts with the source vertex
80+
return path[0] === source ? path : null;
81+
}

0 commit comments

Comments
 (0)