LeetCode-10-正则表达式匹配
in LeetCode with 0 comment

LeetCode-10-正则表达式匹配

in LeetCode with 0 comment

原题地址:正则表达式匹配

给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。

'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素

所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。

说明:

示例 1:

输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。

示例 2:

输入:
s = "aa"
p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。

示例 3:

输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。

示例 4:

输入:
s = "aab"
p = "cab"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。

示例 5:

输入:
s = "mississippi"
p = "mis*is*p*."
输出: false

回溯算法

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

具体到本题中,每次遇到*时就会产生多个分支,因为*意味着它前面的字符可以出现零次或任意多次,每一种选择就是一个分支。我们选择其中的任意一个分支往下走,如果走不通则退回到这个点,重新选择其它分支。

具体的实现代码如下:

/**
 * @param {string} s
 * @param {string} p
 * @return {boolean}
 */
const isMatch1 = function(s, p) {
    // 空匹配串只能匹配空字符串
    if (p.length === 0) {
        return s.length === 0;
    }
    // 匹配第一个字符,注意.号可以当作任意字符使用
    let firstMatch = s.length !== 0 && ['.', s[0]].includes(p[0]);
    if (p.length > 1 && p[1] === '*') {
        // *号表示前面这个字符可以出现零次或多次
        // 零次相当于直接跳过这两个匹配字符
        return isMatch1(s, p.substr(2)) || (firstMatch && isMatch1(s.substr(1), p));
    }
    return firstMatch && isMatch1(s.substr(1), p.substr(1))
};

测试:

let start = new Date();
const test = isMatch1;
console.log(test('aa', 'a')); // false
console.log(test('a', 'a*')); // true
console.log(test('ab', '.*')); // true
console.log(test('aab', 'c*a*b')); // true
console.log(test('mississippi', 'mis*is*p*.')); // false
console.log(new Date().getTime() - start.getTime()); // 6

动态规划

上面的回溯算法我们是采用递归来实现的,所以子串的匹配我们进行了多次重复的计算。优化也很简单,将子串的匹配结果先存储起来,下次需要用到这个结果时直接查询即可,这就是动态归划:

/**
 * @param {string} s
 * @param {string} p
 * @return {boolean}
 */
const isMatch2 = function(s, p) {
    // 数组长度为p的长度加1,因为还要考虑空匹配串
    const dp = new Array(p.length + 1);
    // 要从后往前走,因为要从短的子串往长的匹配,这样长串匹配时才能用来短串的结果
    for (let i = p.length; i >= 0; i --) {
        // 也是长度加1
        dp[i] = new Array(s.length + 1);
        for (let j = s.length; j >= 0; j --) {
            // 空匹配串的处理
            if (i === p.length) {
                dp[i][j] = (j === s.length);
                continue;
            }
            // 这部分与回溯的逻辑相同,只是需要将匹配结果存储起来
            let firstMatch = j !== s.length && ['.', s[j]].includes(p[i]);
            if (i < p.length - 1 && p[i + 1] === '*') {
                dp[i][j] = dp[i + 2][j] || (firstMatch && dp[i][j + 1]);
            } else {
                dp[i][j] = firstMatch && dp[i + 1][j + 1]
            }
        }
    }
    // 返回完整串的匹配结果
    return dp[0][0];
};

测试:

let start = new Date();
const test = isMatch2;
console.log(test('aa', 'a')); // false
console.log(test('a', 'a*')); // true
console.log(test('ab', '.*')); // true
console.log(test('aab', 'c*a*b')); // true
console.log(test('mississippi', 'mis*is*p*.')); // false
console.log(new Date().getTime() - start.getTime()); // 5

时间复杂度: 双重遍历,时间复杂度为O(mn),m和n分别为两个串的长度
空间复杂度: 也是O(mn),因为需要用来二维数组来存储匹配结果