[leetcode 47] 通配符匹配 动态规划vs贪心回溯

时间:2020-03-24
本文章向大家介绍[leetcode 47] 通配符匹配 动态规划vs贪心回溯,主要包括[leetcode 47] 通配符匹配 动态规划vs贪心回溯使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

题目

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

  • '?' 可以匹配任何单个字符。
  • '*' 可以匹配任意字符串(包括空字符串)。
  • 两个字符串完全匹配才算匹配成功。
  • s 可能为空,且只包含从 a-z 的小写字母。
  • p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。

动态规划

考虑匹配的基本情形:

  • 字符相同
  • 模式字符为"?"
  • 模式字符为"*"
  • 其他不匹配情况

如果我们取s和p的最后一个字符比较,其中字符匹配和遇到"?"时,整个字串的匹配结果依赖于前面s[n-1]和p[n-1]个字符的匹配结果。如果遇到"",则依赖前面相邻位置的匹配结果。因此可以建立一个二维矩阵,通过遍历建立整个矩阵的各个位置匹配结果。
这里比较不容易理解的一点,就是当遇到"
"时,我们的dp[i][j]到底应该如何取值
dp[i][j] = dp[i-1][j] || dp[i][j-1] // i为s索引,j为p索引
比较重要的是要考虑遍历的过程,先遍历s,为每一段s遍历p,实际上是为每一小段s去匹配所有的p
如果“”匹配0个字符,很容易想到dp[i][j] = dp[i][j-1]
匹配一个或多个字符时,容易得到dp[i][j] = dp[i-1][j-1]。然而,我们总是先对s[:i-1]遍历完整个p,这个时候如果dp[i-1][j-1]为true,由于p[j]='
',dp[i-1][j]必为true。而此时按照前面的算法,会得到 dp[i-1][j]=dp[i-2][j-1],可能为false。因此我们把这个*看成是前一段s匹配0个字符,得到dp[i][j] = dp[i-1][j-1] = dp[i-1][j]

/**
 * @param {string} s
 * @param {string} p
 * @return {boolean}
 */
var isMatch = function(s, p) {
    // 动态规划
    let row = new Array(p.length+1).fill(false);
    let dp = [];
    // 初始化
    for (let i=0;i<=s.length;i++) {
        dp.push(Object.assign([], row));
    }
    // 首位匹配
    dp[0][0] = true;
    for (let i=0;i<p.length;i++) {
        if (p[i] == '*') {
            dp[0][i+1] = dp[0][i];
        }
    }
    for (let i=0;i<s.length;i++) {
        for (let j=0;j<p.length;j++) {
            if (s[i]==p[j] || p[j]=='?') {
                dp[i+1][j+1] = dp[i][j];
            } else if (p[j] == '*') {
                // 匹配0个字符 dp[i+1][j]
                // 匹配一个或多个字符 dp[i][j+1] ?
                dp[i+1][j+1] = dp[i+1][j] || dp[i][j+1];
            }
        }
    }
    // console.log(dp);
    return dp[s.length][p.length];
};

贪心双指针回溯

动态规划需要生成整个二维矩阵,因此可以优化,只对出现号的位置进行检查。这里必须理解的一点是,当出现多个时,我们只需要对最后一个进行回溯匹配。
比如p = A
BC,如果匹配成功,则 s 中必须有一个或多个B,比如 AxByBzC,我们匹配到AB与AxB成功,之后遇到了新的,那么我们只需要对后一个进行回溯,检查C与yBzC。不需要再考虑AB匹配AxByB成功的情况,理解这一点非常重要。
一旦上述概念理解之后,
算法就非常好写了:

  • s索引i,p索引j,s的回溯位置pre,p的*回溯位置star
  • s[i] == p[j] 或 p[j] == '?', i,j递增
  • 遇到,记录的位置star,s的回溯位置pre。*匹配0个字符即i++
  • 字符不匹配且不是,如果字符串有,s回溯下一位,p回溯*的下一位
  • 字符串没有*,返回false
/**
 * @param {string} s
 * @param {string} p
 * @return {boolean}
 */
var isMatch = function(s, p) {
    // <贪心含义>理解只需要回溯匹配最后一个*
    let s_idx = 0,p_idx = 0;
    let s_pre_idx = -1,p_star_idx = -1;
    while (s_idx < s.length) {
        if (s[s_idx] == p[p_idx] || p[p_idx]=='?') {
            s_idx++;
            p_idx++;
        } else if (p_idx < p.length && p[p_idx] == '*') {
            // 首次匹配0个字符
            s_pre_idx = s_idx;
            p_star_idx = p_idx;
            p_idx++;
        } else if (p_star_idx !== -1) {
            // p回溯到*的下一个位置,s_pre递增
            p_idx = p_star_idx+1;
            s_pre_idx++;
            s_idx = s_pre_idx;
        } else {
            return false;
        }
    }
    // 剩下的p应该全是*
    while (p_idx < p.length) {
        if (p[p_idx] !== '*') {
            return false;
        }
        p_idx++;
    }
    return true;
};

算法效率

第一次提交是动态规划,第二次是贪心双指针。说实话没想到会差距这么大

其他算法

正则表达式的常规思路是有限状态机,实现一个状态机节点,然后根据模式串p构造一个有向图,如果能够从起点走到终点就表示匹配成功。

原文地址:https://www.cnblogs.com/dapianzi/p/12561468.html