Skip to content
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"typecheck": "tsc --noEmit --skipLibCheck",
"lint": "eslint ./**/*.ts",
"lint:fix": "eslint --fix ./**/*.ts",
"prettier": "prettier --write './**/*.{js,ts,tsx}' --ignore-path '.gitignore'"
"prettier": "prettier --write './**/*.{js,ts,tsx}' --ignore-path '.gitignore'",
"check:all": "yarn test && yarn lint && yarn typecheck"
},
"devDependencies": {
"@types/jest": "^30.0.0",
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
163 changes: 163 additions & 0 deletions src/algorithms/graph/dfs/dfs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {dfs, dfsRecursive, reconstructPath} from './dfs';

describe('DFS Algorithm (Iterative)', () => {
test('should traverse 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 {visited, predecessors} = dfs(graph, 0);
// Iterative DFS uses a stack (LIFO), so the last neighbor pushed
// is visited first — yielding a different order from recursive DFS.
expect(visited).toEqual([0, 2, 5, 1, 4, 3]);
expect(predecessors[0]).toBe(-1);
});

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 {visited, predecessors} = dfs(graph, 0);
expect(visited).toEqual([0, 1]);
expect(predecessors[1]).toBe(0);
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(() => dfs(graph, -1)).toThrow('Start vertex is out of range');
expect(() => dfs(graph, 3)).toThrow('Start vertex is out of range');
});

test('should handle a graph with a single vertex', () => {
const graph = [[]];
const {visited, predecessors} = dfs(graph, 0);
expect(visited).toEqual([0]);
expect(predecessors).toEqual([-1]);
});

test('should handle cyclic graphs correctly', () => {
const graph = [
[1, 2], // Edges from vertex 0
[0, 2], // Edges from vertex 1
[0, 1], // Edges from vertex 2
];
const {visited} = dfs(graph, 0);
expect(visited.length).toBe(3);
expect(new Set(visited).size).toBe(3);
expect(visited[0]).toBe(0);
});

test('should handle large graphs efficiently', () => {
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 {visited} = dfs(largeGraph, 0);
const endTime = performance.now();

expect(visited.length).toBe(1000);
expect(visited[0]).toBe(0);
expect(endTime - startTime).toBeLessThan(1000);
});
});

describe('DFS Algorithm (Recursive)', () => {
test('should traverse a simple graph in depth-first order', () => {
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 {visited} = dfsRecursive(graph, 0);
expect(visited[0]).toBe(0);
expect(visited.length).toBe(6);
expect(new Set(visited).size).toBe(6);
// Recursive DFS follows adjacency list order: 0 -> 1 -> 3 -> 4 -> 2 -> 5
expect(visited).toEqual([0, 1, 3, 4, 2, 5]);
});

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 {visited} = dfsRecursive(graph, 0);
expect(visited).toEqual([0, 1]);
});

test('should throw error for invalid start vertex', () => {
const graph = [[1, 2], [0], [0]];
expect(() => dfsRecursive(graph, -1)).toThrow('Start vertex is out of range');
expect(() => dfsRecursive(graph, 3)).toThrow('Start vertex is out of range');
});

test('should handle a graph with a single vertex', () => {
const graph = [[]];
const {visited, predecessors} = dfsRecursive(graph, 0);
expect(visited).toEqual([0]);
expect(predecessors).toEqual([-1]);
});
});

describe('DFS Recursive - stack depth limitation', () => {
test('should throw RangeError on a very deep linear graph', () => {
const depth = 20000;
const deepGraph: number[][] = Array(depth)
.fill(0)
.map((_, i) => (i < depth - 1 ? [i + 1] : []));

expect(() => dfsRecursive(deepGraph, 0)).toThrow(RangeError);
});
});

describe('reconstructPath (DFS)', () => {
test('should reconstruct a valid path', () => {
const graph = [[1, 2], [3], [4], [], []];
const {predecessors} = dfsRecursive(graph, 0);
const path = reconstructPath(0, 3, predecessors);
expect(path).toEqual([0, 1, 3]);
});

test('should handle path from vertex to itself', () => {
const graph = [[1], [2], []];
const {predecessors} = dfsRecursive(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} = dfsRecursive(graph, 0);
const path = reconstructPath(0, 2, predecessors);
expect(path).toBeNull();
});

test('should handle invalid target vertex', () => {
const graph = [[1], [0]];
const {predecessors} = dfsRecursive(graph, 0);
const path = reconstructPath(0, 2, predecessors);
expect(path).toBeNull();
});
});
124 changes: 124 additions & 0 deletions src/algorithms/graph/dfs/dfs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* Performs a depth-first search (DFS) traversal on a graph.
* DFS explores as far as possible along each branch before backtracking.
*
* @param {number[][]} graph - An adjacency list representation of the graph.
* @param {number} start - The starting vertex.
* @returns {{ visited: number[], predecessors: number[] }}
* An object containing the order of visited vertices
* and predecessors array for path reconstruction.
* @throws {Error} If the start vertex is out of range.
*/
export function dfs(
graph: number[][],
start: number,
): {
visited: number[];
predecessors: number[];
} {
if (start < 0 || start >= graph.length) {
throw new Error('Start vertex is out of range');
}
const n = graph.length;
const predecessors: number[] = Array(n).fill(-1);
const seen: boolean[] = Array(n).fill(false);
const visited: number[] = [];
const stack: number[] = [start];
seen[start] = true;

while (stack.length > 0) {
const current = stack.pop()!;
visited.push(current);

for (const neighbor of graph[current]) {
if (!seen[neighbor]) {
seen[neighbor] = true;
predecessors[neighbor] = current;
stack.push(neighbor);
}
}
}

return {visited, predecessors};
}

/**
* Performs a recursive depth-first search (DFS) traversal on a graph.
*
* Note: This implementation uses the call stack for recursion, so it may throw
* a RangeError (Maximum call stack size exceeded) on graphs with depth
* exceeding ~10,000 vertices. For large or deep graphs, use the iterative
* {@link dfs} instead.
*
* @param {number[][]} graph - An adjacency list representation of the graph.
* @param {number} start - The starting vertex.
* @returns {{ visited: number[], predecessors: number[] }}
* An object containing the order of visited vertices
* and predecessors array for path reconstruction.
* @throws {Error} If the start vertex is out of range.
*/
export function dfsRecursive(
graph: number[][],
start: number,
): {
visited: number[];
predecessors: number[];
} {
if (start < 0 || start >= graph.length) {
throw new Error('Start vertex is out of range');
}
const n = graph.length;
const predecessors: number[] = Array(n).fill(-1);
const seen: boolean[] = Array(n).fill(false);
const visited: number[] = [];

function traverse(vertex: number): void {
seen[vertex] = true;
visited.push(vertex);
for (const neighbor of graph[vertex]) {
if (!seen[neighbor]) {
predecessors[neighbor] = vertex;
traverse(neighbor);
}
}
}

traverse(start);
return {visited, predecessors};
}

/**
* Reconstructs the path from a source vertex to a target vertex
* using the predecessors array obtained from DFS.
*
* @param {number} source - The source vertex.
* @param {number} target - The target vertex.
* @param {number[]} predecessors - The predecessors array from DFS.
* @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];
}

return path[0] === source ? path : null;
}
File renamed without changes.
File renamed without changes.
82 changes: 82 additions & 0 deletions src/algorithms/string/rabin-karp/rabin-karp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {rabinKarp} from './rabin-karp';

describe('Rabin-Karp Algorithm', () => {
test('should find a single occurrence', () => {
expect(rabinKarp('hello world', 'world')).toEqual([6]);
});

test('should find multiple occurrences', () => {
expect(rabinKarp('ababababab', 'abab')).toEqual([0, 2, 4, 6]);
});

test('should return empty array when pattern is not found', () => {
expect(rabinKarp('hello world', 'xyz')).toEqual([]);
});

test('should return empty array for empty pattern', () => {
expect(rabinKarp('hello', '')).toEqual([]);
});

test('should return empty array when pattern is longer than text', () => {
expect(rabinKarp('hi', 'hello')).toEqual([]);
});

test('should handle pattern equal to text', () => {
expect(rabinKarp('abc', 'abc')).toEqual([0]);
});

test('should handle single character pattern', () => {
expect(rabinKarp('abcabc', 'a')).toEqual([0, 3]);
});

test('should handle single character text and pattern', () => {
expect(rabinKarp('a', 'a')).toEqual([0]);
expect(rabinKarp('a', 'b')).toEqual([]);
});

test('should handle repeated characters', () => {
expect(rabinKarp('aaaaaa', 'aaa')).toEqual([0, 1, 2, 3]);
});

test('should handle pattern at the end of text', () => {
expect(rabinKarp('abcdef', 'def')).toEqual([3]);
});

test('should handle pattern at the beginning of text', () => {
expect(rabinKarp('abcdef', 'abc')).toEqual([0]);
});

test('should handle large text efficiently', () => {
const text = 'a'.repeat(10000) + 'b';
const pattern = 'a'.repeat(100) + 'b';

const startTime = performance.now();
const result = rabinKarp(text, pattern);
const endTime = performance.now();

expect(result).toEqual([9900]);
expect(endTime - startTime).toBeLessThan(1000);
});

test('should handle hash collisions correctly', () => {
// Use longer strings to increase collision potential
const text = 'The quick brown fox jumps over the lazy dog';
const pattern = 'fox';
expect(rabinKarp(text, pattern)).toEqual([16]);
});

test('should handle non-ASCII characters without integer overflow', () => {
const ch = String.fromCharCode(50000);
const text = ch.repeat(25);
const pattern = ch.repeat(3);
// Should find all 23 overlapping occurrences
const expected = Array.from({length: 23}, (_, i) => i);
expect(rabinKarp(text, pattern)).toEqual(expected);
});

test('should find pattern in Unicode text', () => {
const text = 'こんにちは世界、こんにちは日本';
const pattern = 'こんにちは';
expect(rabinKarp(text, pattern)).toEqual([0, 8]);
});
});
Loading