diff --git a/src/algorithms/math/catalan-number/README.md b/src/algorithms/math/catalan-number/README.md new file mode 100644 index 0000000000..2a0c92bcd0 --- /dev/null +++ b/src/algorithms/math/catalan-number/README.md @@ -0,0 +1,75 @@ +# Catalan Number + +In combinatorial mathematics, the **Catalan numbers** form a sequence of natural numbers that occur in various counting problems, often involving recursively-defined objects. + +The Catalan numbers on nonnegative integers `n` are a sequence `Cn` which begins: + +``` +C0=1, C1=1, C2=2, C3=5, C4=14, C5=42, C6=132, C7=429, C8=1430, C9=4862, C10=16796, ... +``` + +## Formula + +The nth Catalan number can be expressed directly in terms of binomial coefficients: + +![Catalan Formula](https://wikimedia.org/api/rest_v1/media/math/render/svg/2d9f0d0d64c8b5e8f8f8c8f8f8f8f8f8f8f8f8f8) + +``` +C(n) = (2n)! / ((n + 1)! * n!) +``` + +Or using the recursive formula: + +``` +C(0) = 1 +C(n) = sum of C(i) * C(n-1-i) for i = 0 to n-1 +``` + +## Applications + +Catalan numbers have many applications in combinatorics: + +1. **Binary Search Trees**: `Cn` is the number of different binary search trees with `n` keys +2. **Parentheses**: `Cn` is the number of expressions containing `n` pairs of correctly matched parentheses +3. **Polygon Triangulation**: `Cn` is the number of ways to triangulate a convex polygon with `n+2` sides +4. **Path Counting**: `Cn` is the number of paths in a grid from `(0,0)` to `(n,n)` that don't cross the diagonal +5. **Dyck Words**: `Cn` is the number of Dyck words of length `2n` +6. **Mountain Ranges**: `Cn` is the number of "mountain ranges" you can draw using `n` upstrokes and `n` downstrokes + +## Examples + +### Binary Search Trees +With 3 keys (1, 2, 3), there are `C3 = 5` different BSTs: + +``` + 1 1 2 3 3 + \ \ / \ / / + 2 3 1 3 1 2 + \ / \ / + 3 2 2 1 +``` + +### Parentheses +With 3 pairs of parentheses, there are `C3 = 5` valid combinations: + +``` +((())) +(()()) +(())() +()(()) +()()() +``` + +### Polygon Triangulation +A hexagon (6 sides = n+2, so n=4) can be triangulated in `C4 = 14` different ways. + +## Complexity + +- **Time Complexity**: `O(n²)` - Using dynamic programming approach +- **Space Complexity**: `O(n)` - To store intermediate results + +## References + +- [Wikipedia - Catalan Number](https://en.wikipedia.org/wiki/Catalan_number) +- [OEIS - Catalan Numbers](https://oeis.org/A000108) +- [YouTube - Catalan Numbers](https://www.youtube.com/watch?v=GlI17WaMrtw) diff --git a/src/algorithms/math/catalan-number/__test__/catalanNumber.test.js b/src/algorithms/math/catalan-number/__test__/catalanNumber.test.js new file mode 100644 index 0000000000..ef855fa29c --- /dev/null +++ b/src/algorithms/math/catalan-number/__test__/catalanNumber.test.js @@ -0,0 +1,27 @@ +import catalanNumber from '../catalanNumber'; + +describe('catalanNumber', () => { + it('should calculate Catalan numbers correctly', () => { + expect(catalanNumber(0)).toBe(1); + expect(catalanNumber(1)).toBe(1); + expect(catalanNumber(2)).toBe(2); + expect(catalanNumber(3)).toBe(5); + expect(catalanNumber(4)).toBe(14); + expect(catalanNumber(5)).toBe(42); + expect(catalanNumber(6)).toBe(132); + expect(catalanNumber(7)).toBe(429); + expect(catalanNumber(8)).toBe(1430); + expect(catalanNumber(9)).toBe(4862); + expect(catalanNumber(10)).toBe(16796); + }); + + it('should throw error for negative numbers', () => { + expect(() => catalanNumber(-1)).toThrow('Catalan number is not defined for negative numbers'); + expect(() => catalanNumber(-10)).toThrow('Catalan number is not defined for negative numbers'); + }); + + it('should handle larger numbers', () => { + expect(catalanNumber(15)).toBe(9694845); + expect(catalanNumber(20)).toBe(6564120420); + }); +}); diff --git a/src/algorithms/math/catalan-number/catalanNumber.js b/src/algorithms/math/catalan-number/catalanNumber.js new file mode 100644 index 0000000000..0eb0b0714d --- /dev/null +++ b/src/algorithms/math/catalan-number/catalanNumber.js @@ -0,0 +1,48 @@ +/** + * Calculate nth Catalan number using dynamic programming approach. + * + * Catalan numbers form a sequence of natural numbers that occur in various + * counting problems, often involving recursively-defined objects. + * + * The nth Catalan number can be expressed directly in terms of binomial coefficients: + * C(n) = (2n)! / ((n + 1)! * n!) + * + * Or using the recursive formula: + * C(0) = 1 + * C(n) = sum of C(i) * C(n-1-i) for i = 0 to n-1 + * + * Applications: + * - Number of different Binary Search Trees with n keys + * - Number of expressions containing n pairs of parentheses + * - Number of ways to triangulate a polygon with n+2 sides + * - Number of paths in a grid from (0,0) to (n,n) without crossing diagonal + * + * @param {number} n - The position in Catalan sequence + * @return {number} - The nth Catalan number + */ +export default function catalanNumber(n) { + // Handle edge cases + if (n < 0) { + throw new Error('Catalan number is not defined for negative numbers'); + } + + // Base case + if (n === 0 || n === 1) { + return 1; + } + + // Use dynamic programming to calculate Catalan number + // This approach has O(n^2) time complexity and O(n) space complexity + const catalan = new Array(n + 1).fill(0); + catalan[0] = 1; + catalan[1] = 1; + + // Calculate Catalan numbers from 2 to n + for (let i = 2; i <= n; i += 1) { + for (let j = 0; j < i; j += 1) { + catalan[i] += catalan[j] * catalan[i - 1 - j]; + } + } + + return catalan[n]; +} diff --git a/src/algorithms/sorting/pancake-sort/PancakeSort.js b/src/algorithms/sorting/pancake-sort/PancakeSort.js new file mode 100644 index 0000000000..592d0b558e --- /dev/null +++ b/src/algorithms/sorting/pancake-sort/PancakeSort.js @@ -0,0 +1,63 @@ +/* eslint-disable no-param-reassign */ +import Sort from '../Sort'; + +export default class PancakeSort extends Sort { + /** + * Flip the array from index 0 to index i + * @param {*[]} array + * @param {number} endIndex + */ + flip(array, endIndex) { + let start = 0; + let end = endIndex; + while (start < end) { + // Swap elements + [array[start], array[end]] = [array[end], array[start]]; + start += 1; + end -= 1; + } + } + + /** + * Find the index of the maximum element in array[0...n] + * @param {*[]} array + * @param {number} n + * @return {number} + */ + findMaxIndex(array, n) { + let maxIndex = 0; + for (let i = 1; i <= n; i += 1) { + // Call visiting callback. + this.callbacks.visitingCallback(array[i]); + + if (this.comparator.greaterThan(array[i], array[maxIndex])) { + maxIndex = i; + } + } + return maxIndex; + } + + sort(originalArray) { + // Clone original array to prevent its modification. + const array = [...originalArray]; + + // Start from the complete array and one by one reduce current size by one + for (let currentSize = array.length - 1; currentSize > 0; currentSize -= 1) { + // Find index of the maximum element in array[0...currentSize] + const maxIndex = this.findMaxIndex(array, currentSize); + + // Move the maximum element to end of current array if it's not already at the end + if (maxIndex !== currentSize) { + // First move maximum number to beginning if it's not already + if (maxIndex !== 0) { + this.flip(array, maxIndex); + } + + // Now move the maximum number to end by reversing current array + this.flip(array, currentSize); + } + } + + return array; + } +} diff --git a/src/algorithms/sorting/pancake-sort/README.md b/src/algorithms/sorting/pancake-sort/README.md new file mode 100644 index 0000000000..9e911c4d01 --- /dev/null +++ b/src/algorithms/sorting/pancake-sort/README.md @@ -0,0 +1,42 @@ +# Pancake Sort + +Pancake sorting is a sorting algorithm in which the only allowed operation is to "flip" one end of the list. The goal is to sort the array by repeatedly flipping portions of it. + +The algorithm is inspired by the real-world problem of sorting a stack of pancakes by size using a spatula. A flip operation reverses the order of elements from the beginning of the array up to a specified index. + +![Pancake Sort](https://upload.wikimedia.org/wikipedia/commons/0/0f/Pancake_sort_operation.png) + +## Algorithm + +The algorithm works by repeatedly finding the maximum element and moving it to its correct position at the end of the array through a series of flips: + +1. Find the maximum element in the unsorted portion of the array +2. If it's not at the beginning, flip it to the beginning +3. Flip it to its correct position at the end +4. Reduce the size of the unsorted portion and repeat + +## Example + +Given an array: `[3, 6, 2, 8, 4, 5]` + +**Step 1:** Find max (8) at index 3 +- Flip to beginning: `[8, 6, 3, 2, 4, 5]` +- Flip to end: `[5, 4, 2, 3, 6, 8]` + +**Step 2:** Find max (6) at index 4 +- Flip to beginning: `[6, 4, 2, 3, 5, 8]` +- Flip to position 4: `[5, 3, 2, 4, 6, 8]` + +**Step 3:** Continue until sorted: `[2, 3, 4, 5, 6, 8]` + +## Complexity + +| Name | Best | Average | Worst | Memory | Stable | Comments | +| --------------------- | :-------------: | :-----------------: | :-----------------: | :-------: | :-------: | :-------- | +| **Pancake sort** | n | n2 | n2 | 1 | No | | + +## References + +- [Wikipedia](https://en.wikipedia.org/wiki/Pancake_sorting) +- [GeeksforGeeks](https://www.geeksforgeeks.org/pancake-sorting/) +- [YouTube](https://www.youtube.com/watch?v=kk-_DDgoXfg) diff --git a/src/algorithms/sorting/pancake-sort/__test__/PancakeSort.test.js b/src/algorithms/sorting/pancake-sort/__test__/PancakeSort.test.js new file mode 100644 index 0000000000..414052f190 --- /dev/null +++ b/src/algorithms/sorting/pancake-sort/__test__/PancakeSort.test.js @@ -0,0 +1,60 @@ +import PancakeSort from '../PancakeSort'; +import { + equalArr, + notSortedArr, + reverseArr, + sortedArr, + SortTester, +} from '../../SortTester'; + +// Complexity constants. +const SORTED_ARRAY_VISITING_COUNT = 190; +const NOT_SORTED_ARRAY_VISITING_COUNT = 190; +const REVERSE_SORTED_ARRAY_VISITING_COUNT = 190; +const EQUAL_ARRAY_VISITING_COUNT = 190; + +describe('PancakeSort', () => { + it('should sort array', () => { + SortTester.testSort(PancakeSort); + }); + + it('should sort array with custom comparator', () => { + SortTester.testSortWithCustomComparator(PancakeSort); + }); + + it('should sort negative numbers', () => { + SortTester.testNegativeNumbersSort(PancakeSort); + }); + + it('should visit EQUAL array element specified number of times', () => { + SortTester.testAlgorithmTimeComplexity( + PancakeSort, + equalArr, + EQUAL_ARRAY_VISITING_COUNT, + ); + }); + + it('should visit SORTED array element specified number of times', () => { + SortTester.testAlgorithmTimeComplexity( + PancakeSort, + sortedArr, + SORTED_ARRAY_VISITING_COUNT, + ); + }); + + it('should visit NOT SORTED array element specified number of times', () => { + SortTester.testAlgorithmTimeComplexity( + PancakeSort, + notSortedArr, + NOT_SORTED_ARRAY_VISITING_COUNT, + ); + }); + + it('should visit REVERSE SORTED array element specified number of times', () => { + SortTester.testAlgorithmTimeComplexity( + PancakeSort, + reverseArr, + REVERSE_SORTED_ARRAY_VISITING_COUNT, + ); + }); +}); diff --git a/src/algorithms/string/longest-common-subsequence/README.md b/src/algorithms/string/longest-common-subsequence/README.md new file mode 100644 index 0000000000..db22972ee8 --- /dev/null +++ b/src/algorithms/string/longest-common-subsequence/README.md @@ -0,0 +1,101 @@ +# Longest Common Subsequence (LCS) + +The **Longest Common Subsequence (LCS)** problem is finding the longest subsequence common to two sequences. A subsequence is a sequence that can be derived from another sequence by deleting some or no elements without changing the order of the remaining elements. + +## Definition + +Given two sequences `X` and `Y`, a sequence `Z` is a common subsequence of `X` and `Y` if `Z` is a subsequence of both `X` and `Y`. + +For example: +- If `X = "ABCDGH"` and `Y = "AEDFHR"`, then `"ADH"` is a common subsequence with length 3 +- If `X = "AGGTAB"` and `Y = "GXTXAYB"`, then `"GTAB"` is a common subsequence with length 4 + +## Difference from Longest Common Substring + +**Important distinction:** +- **Subsequence**: Characters don't need to be contiguous (can skip characters) +- **Substring**: Characters must be contiguous (no gaps allowed) + +Example: +- For strings "ABCDGH" and "AEDFHR": + - LCS (subsequence): "ADH" ✓ (can skip characters) + - LCS (substring): "" (no common contiguous substring of length > 1) + +## Algorithm + +This implementation uses **dynamic programming** with a bottom-up approach: + +1. Create a 2D table `dp[m+1][n+1]` where `m` and `n` are lengths of the two strings +2. Fill the table using the recurrence relation: + ``` + dp[i][j] = dp[i-1][j-1] + 1 if str1[i-1] == str2[j-1] + dp[i][j] = max(dp[i-1][j], dp[i][j-1]) otherwise + ``` +3. Backtrack from `dp[m][n]` to construct the actual LCS string + +## Complexity + +- **Time Complexity**: `O(m * n)` where `m` and `n` are the lengths of the two strings +- **Space Complexity**: `O(m * n)` for the DP table + +## Applications + +The LCS algorithm has many practical applications: + +1. **Version Control Systems**: Git uses LCS-based algorithms to compute diffs between file versions +2. **Bioinformatics**: DNA sequence alignment and comparison +3. **Plagiarism Detection**: Finding similar content between documents +4. **File Comparison**: Tools like `diff` use LCS to show differences +5. **Data Synchronization**: Determining minimal changes needed to sync data +6. **Spell Checkers**: Finding similar words for suggestions + +## Examples + +### Example 1: Basic LCS +```javascript +longestCommonSubsequence('ABCDGH', 'AEDFHR'); +// Returns: 'ADH' +// Explanation: A-D-H are common in both strings in order +``` + +### Example 2: Programming Terms +```javascript +longestCommonSubsequence('algorithms', 'logarithm'); +// Returns: 'lgrithm' +``` + +### Example 3: No Common Subsequence +```javascript +longestCommonSubsequence('ABC', 'DEF'); +// Returns: '' +``` + +### Example 4: One String is Subsequence of Another +```javascript +longestCommonSubsequence('ABCDEF', 'ACE'); +// Returns: 'ACE' +``` + +## Visualization + +For strings `X = "AGGTAB"` and `Y = "GXTXAYB"`: + +``` + "" G X T X A Y B +"" 0 0 0 0 0 0 0 0 +A 0 0 0 0 0 1 1 1 +G 0 1 1 1 1 1 1 1 +G 0 1 1 1 1 1 1 1 +T 0 1 1 2 2 2 2 2 +A 0 1 1 2 2 3 3 3 +B 0 1 1 2 2 3 3 4 +``` + +The value at `dp[6][7] = 4` indicates the LCS length is 4. +Backtracking gives us: `"GTAB"` + +## References + +- [Wikipedia - Longest Common Subsequence](https://en.wikipedia.org/wiki/Longest_common_subsequence_problem) +- [GeeksforGeeks - LCS](https://www.geeksforgeeks.org/longest-common-subsequence-dp-4/) +- [YouTube - Dynamic Programming LCS](https://www.youtube.com/watch?v=ASoaQq66foQ) diff --git a/src/algorithms/string/longest-common-subsequence/__test__/longestCommonSubsequence.test.js b/src/algorithms/string/longest-common-subsequence/__test__/longestCommonSubsequence.test.js new file mode 100644 index 0000000000..e1f1f1f84b --- /dev/null +++ b/src/algorithms/string/longest-common-subsequence/__test__/longestCommonSubsequence.test.js @@ -0,0 +1,42 @@ +import longestCommonSubsequence from '../longestCommonSubsequence'; + +describe('longestCommonSubsequence', () => { + it('should find LCS of two strings', () => { + expect(longestCommonSubsequence('', '')).toBe(''); + expect(longestCommonSubsequence('ABC', '')).toBe(''); + expect(longestCommonSubsequence('', 'ABC')).toBe(''); + expect(longestCommonSubsequence('A', 'A')).toBe('A'); + expect(longestCommonSubsequence('ABC', 'ABC')).toBe('ABC'); + expect(longestCommonSubsequence('ABCDGH', 'AEDFHR')).toBe('ADH'); + expect(longestCommonSubsequence('AGGTAB', 'GXTXAYB')).toBe('GTAB'); + expect(longestCommonSubsequence('sea', 'eat')).toBe('ea'); + expect(longestCommonSubsequence('algorithms', 'logarithm')).toBe('lorithm'); + }); + + it('should handle strings with no common subsequence', () => { + expect(longestCommonSubsequence('ABC', 'DEF')).toBe(''); + expect(longestCommonSubsequence('XYZ', 'PQR')).toBe(''); + }); + + it('should handle strings where one is subsequence of another', () => { + expect(longestCommonSubsequence('ABCDEF', 'ACE')).toBe('ACE'); + expect(longestCommonSubsequence('ACE', 'ABCDEF')).toBe('ACE'); + }); + + it('should handle repeated characters', () => { + expect(longestCommonSubsequence('AAA', 'AA')).toBe('AA'); + expect(longestCommonSubsequence('AAAA', 'AA')).toBe('AA'); + expect(longestCommonSubsequence('ABABA', 'BABA')).toBe('BABA'); + }); + + it('should handle case sensitivity', () => { + expect(longestCommonSubsequence('ABC', 'abc')).toBe(''); + expect(longestCommonSubsequence('Hello', 'hello')).toBe('ello'); + }); + + it('should handle longer strings', () => { + expect(longestCommonSubsequence('ABCBDAB', 'BDCABA')).toBe('BDAB'); + expect(longestCommonSubsequence('programming', 'gaming')).toBe('gaming'); + expect(longestCommonSubsequence('dynamic', 'programming')).toBe('ami'); + }); +}); diff --git a/src/algorithms/string/longest-common-subsequence/longestCommonSubsequence.js b/src/algorithms/string/longest-common-subsequence/longestCommonSubsequence.js new file mode 100644 index 0000000000..54e0da3ceb --- /dev/null +++ b/src/algorithms/string/longest-common-subsequence/longestCommonSubsequence.js @@ -0,0 +1,74 @@ +/** + * Find the longest common subsequence (LCS) of two strings. + * + * A subsequence is a sequence that can be derived from another sequence + * by deleting some or no elements without changing the order of the + * remaining elements. + * + * For example: + * - LCS of "ABCDGH" and "AEDFHR" is "ADH" (length 3) + * - LCS of "AGGTAB" and "GXTXAYB" is "GTAB" (length 4) + * + * This implementation uses dynamic programming with O(m*n) time complexity + * and O(m*n) space complexity, where m and n are the lengths of the two strings. + * + * Applications: + * - Diff utilities (comparing file versions) + * - DNA sequence analysis + * - Version control systems + * - Plagiarism detection + * - Data comparison tools + * + * @param {string} str1 - First string + * @param {string} str2 - Second string + * @return {string} - The longest common subsequence + */ +export default function longestCommonSubsequence(str1, str2) { + // Handle edge cases + if (!str1 || !str2) { + return ''; + } + + const m = str1.length; + const n = str2.length; + + // Create a 2D array to store lengths of LCS + // dp[i][j] contains length of LCS of str1[0..i-1] and str2[0..j-1] + const dp = Array(m + 1) + .fill(null) + .map(() => Array(n + 1).fill(0)); + + // Build the dp table in bottom-up fashion + for (let i = 1; i <= m; i += 1) { + for (let j = 1; j <= n; j += 1) { + if (str1[i - 1] === str2[j - 1]) { + // If characters match, add 1 to the result of smaller strings + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + // If characters don't match, take the maximum of two possibilities + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + // Backtrack to find the actual LCS string + let i = m; + let j = n; + const lcs = []; + + while (i > 0 && j > 0) { + if (str1[i - 1] === str2[j - 1]) { + // If characters match, it's part of LCS + lcs.unshift(str1[i - 1]); + i -= 1; + j -= 1; + } else if (dp[i - 1][j] > dp[i][j - 1]) { + // Move in the direction of larger value + i -= 1; + } else { + j -= 1; + } + } + + return lcs.join(''); +}