diff --git a/README.md b/README.md index 8100324e..685f4122 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,11 @@ * [Least Common Multiple](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/math/least-common-multiple) (LCM) * [Fisher–Yates Shuffle](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/math/fisher-yates) - random permutation of a finite sequence * **String** - * [Levenshtein Distance](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/string/levenshtein-distance) - minimum edit distance between two sequences (DP approach) + * [Levenshtein Distance](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/string/levenshtein-distance) - minimum edit distance between two sequences (dynamic programming approach) * [Hamming Distance](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/string/hamming-distance) - number of positions at which the symbols are different * [Knuth–Morris–Pratt Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/string/knuth-morris-pratt) - substring search * [Rabin Karp Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/string/rabin-karp) - substring search - * Longest common subsequence + * [Longest Common Subsequence](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/string/longest-common-subsequnce) (LCS) * longest common substring * **Search** * [Binary Search](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/search/binary-search) diff --git a/src/algorithms/string/longest-common-subsequnce/README.md b/src/algorithms/string/longest-common-subsequnce/README.md new file mode 100644 index 00000000..8427072e --- /dev/null +++ b/src/algorithms/string/longest-common-subsequnce/README.md @@ -0,0 +1,25 @@ +# Longest common subsequence problem + +The longest common subsequence (LCS) problem is the problem of finding +the longest subsequence common to all sequences in a set of sequences +(often just two sequences). It differs from the longest common substring +problem: unlike substrings, subsequences are not required to occupy +consecutive positions within the original sequences. + +## Application + +The longest common subsequence problem is a classic computer science +problem, the basis of data comparison programs such as the diff utility, +and has applications in bioinformatics. It is also widely used by +revision control systems such as Git for reconciling multiple changes +made to a revision-controlled collection of files. + +## Example + +- LCS for input Sequences `ABCDGH` and `AEDFHR` is `ADH` of length 3. +- LCS for input Sequences `AGGTAB` and `GXTXAYB` is `GTAB` of length 4. + +## References + +- [Wikipedia](https://en.wikipedia.org/wiki/Longest_common_subsequence_problem) +- [YouTube](https://www.youtube.com/watch?v=NnD96abizww) diff --git a/src/algorithms/string/longest-common-subsequnce/__test__/longestCommonSubsequnce.test.js b/src/algorithms/string/longest-common-subsequnce/__test__/longestCommonSubsequnce.test.js new file mode 100644 index 00000000..7be969ee --- /dev/null +++ b/src/algorithms/string/longest-common-subsequnce/__test__/longestCommonSubsequnce.test.js @@ -0,0 +1,13 @@ +import longestCommonSubsequnce from '../longestCommonSubsequnce'; + +describe('longestCommonSubsequnce', () => { + it('should find longest common subsequence for two strings', () => { + expect(longestCommonSubsequnce('', '')).toBe(''); + expect(longestCommonSubsequnce('', 'ABC')).toBe(''); + expect(longestCommonSubsequnce('ABC', '')).toBe(''); + expect(longestCommonSubsequnce('ABC', 'DEFG')).toBe(''); + expect(longestCommonSubsequnce('ABCDGH', 'AEDFHR')).toBe('ADH'); + expect(longestCommonSubsequnce('AGGTAB', 'GXTXAYB')).toBe('GTAB'); + expect(longestCommonSubsequnce('ABCDAF', 'ACBCF')).toBe('ABCF'); + }); +}); diff --git a/src/algorithms/string/longest-common-subsequnce/longestCommonSubsequnce.js b/src/algorithms/string/longest-common-subsequnce/longestCommonSubsequnce.js new file mode 100644 index 00000000..94c6c4d9 --- /dev/null +++ b/src/algorithms/string/longest-common-subsequnce/longestCommonSubsequnce.js @@ -0,0 +1,60 @@ +/** + * @param {string} s1 + * @param {string} s2 + * @return {string} + */ +export default function longestCommonSubsequnce(s1, s2) { + // Init LCS matrix. + const lcsMatrix = Array(s2.length + 1).fill(null).map(() => Array(s1.length + 1).fill(null)); + + // Fill first row with zeros. + for (let columnIndex = 0; columnIndex <= s1.length; columnIndex += 1) { + lcsMatrix[0][columnIndex] = 0; + } + + // Fill first column with zeros. + for (let rowIndex = 0; rowIndex <= s2.length; rowIndex += 1) { + lcsMatrix[rowIndex][0] = 0; + } + + // Fill rest of the column that correspond to each of two strings. + for (let rowIndex = 1; rowIndex <= s2.length; rowIndex += 1) { + for (let columnIndex = 1; columnIndex <= s1.length; columnIndex += 1) { + if (s1[columnIndex - 1] === s2[rowIndex - 1]) { + lcsMatrix[rowIndex][columnIndex] = lcsMatrix[rowIndex - 1][columnIndex - 1] + 1; + } else { + lcsMatrix[rowIndex][columnIndex] = Math.max( + lcsMatrix[rowIndex - 1][columnIndex], + lcsMatrix[rowIndex][columnIndex - 1], + ); + } + } + } + + // Calculate LCS based on LCS matrix. + if (!lcsMatrix[s2.length][s1.length]) { + // If the length of largest common string is zero then return empty string. + return ''; + } + + let lcs = ''; + let columnIndex = s1.length; + let rowIndex = s2.length; + + while (columnIndex > 0 || rowIndex > 0) { + if (s1[columnIndex - 1] === s2[rowIndex - 1]) { + // Move by diagonal left-top. + lcs = s1[columnIndex - 1] + lcs; + columnIndex -= 1; + rowIndex -= 1; + } else if (lcsMatrix[rowIndex][columnIndex] === lcsMatrix[rowIndex][columnIndex - 1]) { + // Move left. + columnIndex -= 1; + } else { + // Move up. + rowIndex -= 1; + } + } + + return lcs; +}