|
| 1 | +const ZERO_OR_MORE_CHARS = '*'; |
| 2 | +const ANY_CHAR = '.'; |
| 3 | + |
| 4 | +/** |
| 5 | + * Dynamic programming approach. |
| 6 | + * |
| 7 | + * @param {string} string |
| 8 | + * @param {string} pattern |
| 9 | + * @return {boolean} |
| 10 | + */ |
| 11 | +export default function regularExpressionMatching(string, pattern) { |
| 12 | + /* |
| 13 | + * Let's initiate dynamic programming matrix for this string and pattern. |
| 14 | + * We will have pattern characters on top (as columns) and string characters |
| 15 | + * will be placed to the left of the table (as rows). |
| 16 | + * |
| 17 | + * Example: |
| 18 | + * |
| 19 | + * a * b . b |
| 20 | + * - - - - - - |
| 21 | + * a - - - - - - |
| 22 | + * a - - - - - - |
| 23 | + * b - - - - - - |
| 24 | + * y - - - - - - |
| 25 | + * b - - - - - - |
| 26 | + */ |
| 27 | + const matchMatrix = Array(string.length + 1).fill(null).map(() => { |
| 28 | + return Array(pattern.length + 1).fill(null); |
| 29 | + }); |
| 30 | + |
| 31 | + // Let's fill the top-left cell with true. This would mean that empty |
| 32 | + // string '' matches to empty pattern ''. |
| 33 | + matchMatrix[0][0] = true; |
| 34 | + |
| 35 | + // Let's fill the first row of the matrix with false. That would mean that |
| 36 | + // empty string can't match any non-empty pattern. |
| 37 | + // |
| 38 | + // Example: |
| 39 | + // string: '' |
| 40 | + // pattern: 'a.z' |
| 41 | + // |
| 42 | + // The one exception here is patterns like a*b* that matches the empty string. |
| 43 | + for (let columnIndex = 1; columnIndex <= pattern.length; columnIndex += 1) { |
| 44 | + const patternIndex = columnIndex - 1; |
| 45 | + |
| 46 | + if (pattern[patternIndex] === ZERO_OR_MORE_CHARS) { |
| 47 | + matchMatrix[0][columnIndex] = matchMatrix[0][columnIndex - 2]; |
| 48 | + } else { |
| 49 | + matchMatrix[0][columnIndex] = false; |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + // Let's fill the first column with false. That would mean that empty pattern |
| 54 | + // can't match any non-empty string. |
| 55 | + // |
| 56 | + // Example: |
| 57 | + // string: 'ab' |
| 58 | + // pattern: '' |
| 59 | + for (let rowIndex = 1; rowIndex <= string.length; rowIndex += 1) { |
| 60 | + matchMatrix[rowIndex][0] = false; |
| 61 | + } |
| 62 | + |
| 63 | + // Not let's go through every letter of the pattern and every letter of |
| 64 | + // the string and compare them one by one. |
| 65 | + for (let rowIndex = 1; rowIndex <= string.length; rowIndex += 1) { |
| 66 | + for (let columnIndex = 1; columnIndex <= pattern.length; columnIndex += 1) { |
| 67 | + // Take into account that fact that matrix contain one extra column and row. |
| 68 | + const stringIndex = rowIndex - 1; |
| 69 | + const patternIndex = columnIndex - 1; |
| 70 | + |
| 71 | + if (pattern[patternIndex] === ZERO_OR_MORE_CHARS) { |
| 72 | + /* |
| 73 | + * In case if current pattern character is special '*' character we have |
| 74 | + * two options: |
| 75 | + * |
| 76 | + * 1. Since * char allows it previous char to not be presented in a string we |
| 77 | + * need to check if string matches the pattern without '*' char and without the |
| 78 | + * char that goes before '*'. That would mean to go two positions left on the |
| 79 | + * same row. |
| 80 | + * |
| 81 | + * 2. Since * char allows it previous char to be presented in a string many times we |
| 82 | + * need to check if char before * is the same as current string char. If they are the |
| 83 | + * same that would mean that current string matches the current pattern in case if |
| 84 | + * the string WITHOUT current char matches the same pattern. This would mean to go |
| 85 | + * one position up in the same row. |
| 86 | + */ |
| 87 | + if (matchMatrix[rowIndex][columnIndex - 2] === true) { |
| 88 | + matchMatrix[rowIndex][columnIndex] = true; |
| 89 | + } else if ( |
| 90 | + ( |
| 91 | + pattern[patternIndex - 1] === string[stringIndex] || |
| 92 | + pattern[patternIndex - 1] === ANY_CHAR |
| 93 | + ) && |
| 94 | + matchMatrix[rowIndex - 1][columnIndex] === true |
| 95 | + ) { |
| 96 | + matchMatrix[rowIndex][columnIndex] = true; |
| 97 | + } else { |
| 98 | + matchMatrix[rowIndex][columnIndex] = false; |
| 99 | + } |
| 100 | + } else if ( |
| 101 | + pattern[patternIndex] === string[stringIndex] || |
| 102 | + pattern[patternIndex] === ANY_CHAR |
| 103 | + ) { |
| 104 | + /* |
| 105 | + * In case if current pattern char is the same as current string char |
| 106 | + * or it may be any character (in case if pattern contains '.' char) |
| 107 | + * we need to check if there was a match for the pattern and for the |
| 108 | + * string by WITHOUT current char. This would mean that we may copy |
| 109 | + * left-top diagonal value. |
| 110 | + * |
| 111 | + * Example: |
| 112 | + * |
| 113 | + * a b |
| 114 | + * a 1 - |
| 115 | + * b - 1 |
| 116 | + */ |
| 117 | + matchMatrix[rowIndex][columnIndex] = matchMatrix[rowIndex - 1][columnIndex - 1]; |
| 118 | + } else { |
| 119 | + /* |
| 120 | + * In case if pattern char and string char are different we may |
| 121 | + * treat this case as "no-match". |
| 122 | + * |
| 123 | + * Example: |
| 124 | + * |
| 125 | + * a b |
| 126 | + * a - - |
| 127 | + * c - 0 |
| 128 | + */ |
| 129 | + matchMatrix[rowIndex][columnIndex] = false; |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + return matchMatrix[string.length][pattern.length]; |
| 135 | +} |
0 commit comments