• Hard | LeetCode 10 | 剑指 Offer19. 正则表达式匹配 | 递归 | 动态规划


    请实现一个函数用来匹配包含'. ''*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a""ab*ac*a"匹配,但与"aa.a""ab*a"均不匹配。

    示例 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 = "c*a*b"
    输出: true
    解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
    

    示例 5:

    输入:
    s = "mississippi"
    p = "mis*is*p*."
    输出: false
    
    • s 可能为空,且只包含从 a-z 的小写字母。
    • p 可能为空,且只包含从 a-z 的小写字母以及字符 .*,无连续的 '*'

    方法一:递归

    维持两个指针sIndex, pIndex 分别指向主串和模式串, 从左往右比较。

    1. 如果pIndex指向的元素的下一个元素不是'*', 则说明当前的模式串字符必须参与匹配, 且匹配一次, 然后继续匹配后面未匹配的字符。用代码语言表达为

      if (pIndex == p.length() - 1 || p.charAt(pIndex+1) != '*') {
          // 下一个字符不是'*'
          if (sIndex < s.length() &&(p.charAt(pIndex) == s.charAt(sIndex) || p.charAt(pIndex) =='.')) {
              // 匹配主串和模式串后面的字符
              return matchCore(s, sIndex + 1, p, pIndex + 1);
          }
      }
      
    2. 如果pIndex指向的元素的下一个元素是'*', 则说明当前的pIndex元素可以参与匹配0次 或者参与匹配多次。

      2.1 如果当前的pIndex元素参与匹配0次 , 那么当前的主串字符在下一次递归还是要参与匹配的, 而且对于模式串, 下一次递归里, 当前字符和下一个'*'都不参与匹配。用代码语言表达为 return matchCore(s, sIndex, p, pIndex + 2);

      2.2 如果当前的pIndex元素参与匹配, 可能1, 2,3,4... 多次。则当前主串字符在下一次递归就无需再匹配了, 因为当前的pIndex元素已经匹配了。如果是只匹配一次, 当前的模式串字符和下一个'*'号在下一次递归可以不匹配了, 但如果是2, 3, 4...等多次, 那么还是要在下一次递归继续使用。综合来讲, 可以写为matchCore(s, sIndex + 1, p, pIndex), 因为即使下一个'*'号只代表当前字符串出现1次, 把这个'*'号保留到下一次递归当中, 下一次递归如果不需要此字符, '*'号是可以代表当前当前字符出现0次的。所以将'*'代表前一个字符出现1, 2,3..等多次写成matchCore(s, sIndex + 1, p, pIndex)也是没有问题的。

    3. 前面1,2两点是递归的具体逻辑。而递归出口就比较容易想到了。

      3.1 如果主串和模式串刚好全部匹配完, 自然返回true

      3.2 如果模式串已经匹配完, 主串还没有匹配完, 那么返回false。

      3.3 如果是主串已经全部匹配完, 模式串还没有匹配完, 那么这个时候是否能直接返回false呢?答案是不可以。反例就是:主串"", 模式串a*

    所以即找到了递归的关系(前面第1, 2点), 也找到了递归出口(前面第3点), 下面的代码自然就水到渠成了。

    public boolean isMatch(String s, String p) {
        if (s == null || p == null) {
        }
        return matchCore(s, 0, p, 0);
        
    }
    
    public boolean matchCore(String s, int sIndex, String p, int pIndex) {
        if (sIndex == s.length() && pIndex == p.length()) {
            return true;
        }
        // 模式串全部匹配完就会立即判断是否能够匹配
        if ((sIndex < s.length() && pIndex == p.length())) {
            return false;
        }
    
        // 模式串的下一个不是 * , 或者模式串已经匹配到最后一个字符 的情况
        if (pIndex == p.length() - 1 || p.charAt(pIndex+1) != '*') {
            if (sIndex < s.length() && // 确保主串指针不能越界, 因为即使主串已经全部匹配完, 如果模式串还没有匹配, 还是要继续匹配的
                (p.charAt(pIndex) == s.charAt(sIndex) || p.charAt(pIndex) == '.')) {
                return matchCore(s, sIndex + 1, p, pIndex + 1);
            }
        } else {
            // 模式串下一个是 * 的情况
    
            // 当前模式串的字符, 能够匹配当前字符串字符
            if (sIndex < s.length() && (p.charAt(pIndex) == s.charAt(sIndex) || p.charAt(pIndex) == '.')) {
                // 可以选择 * 匹配当前字符串字符1次或多次, 当前模式串字符还要继续匹配后面的字符串
                return matchCore(s, sIndex + 1, p, pIndex) ||
                        // * 匹配当前字符串字符 0 次
                        matchCore(s, sIndex, p, pIndex + 2);
            } else {
                // 当前模式串字符 无法匹配 当前字符串字符 那么* 只能出现代表前面字符 0 次
                return matchCore(s, sIndex, p, pIndex + 2);
            }
        }
        return false;
    }
    

    方法二: 动态规划

    既然是动态规划, 我首先给一个二维表格boolean[][] match = new boolean[s.length() + 1][p.length() + 1];其中match[i][j]代表从 主串i以及左边的字符串 与 模式串里j以及左边的字符串 能够匹配上。所以动态规划的递归关系就分为以下两种情况

    1. 如果当前字符不是'*', 那么当前match值取决于 (1) 当前的主串和模式串的字符(单个) 是否能够匹配; (2) 如果能够匹配了, 还要看他们前面的那些字符是否能够匹配上, 使用代码语言表达为

      if (p.charAt(j-1) != '*') {
          match[i][j] = matches(s, i, p, j) && match[i-1][j-1];
      }
      

      其中判断某两个字符是否能够匹配的方法如下 , 注意, 在进行遍历的时候, 第一个字符的下标是从1开始里, 这里要相应的处理。

      public boolean matches(String s, int i, String  p, int j) {
          if (i == 0) {
              return false;
          }
          return s.charAt(i-1) == p.charAt(j-1) || p.charAt(j-1) == '.';
      }
      
    2. 如果当前字符是'*'。这种情况就要分两种情况讨论了。
      2.1 当前'*'字符的前一个字符出现次数为0, 那么当前的match[i][j]值, 完全等价于match[i][j-2], 这个应该很容易理解
      2.2 当前'*'字符的前一个字符出现次数不为0, 出现1, 2,3....等多次, 那么当前的match[i][j]值即取决于:(1) 当前主串的s[i]的值与模式串*号前一个值p[j-1]的值是否能够匹配; (2) 主串前面的值 与 模式串前面的值(包含当前的'*') 是否能够匹配。(至于为什么即使*表示前一个字符只出现1次, 比较前面的字符串 也要包含在内 ? 原因和第一种递归里的方法一样, 在前一次匹配当中, 这里的*同样是可以代表前一个字符出现次数为0的);

      这两种情况用代码语言表达为:

      match[i][j] = match[i][j-2] || // * 的前一个字符匹配0次
                        (matches(s, i, p, j-1) && match[i-1][j]); // * 的前一个字符匹配1次或者多次
      
    3. 动态规划的初始边界。

      3.1 match[0][0] = true, 表示当主串和模式串都没有字符时, 为true。

      3.2 循环遍历起始指针如下, 主串是从下标0开始的, 原因是即使主串没有字符, 模式串有字符, 比如主串:"", 模式串"a*", 这个时候match[0][1]和match[0][2] 都是true, 也是需要进行计算的。

      for(int i = 0; i <= s.length(); i++) {
          for (int j = 1; j <= p.length(); j++) {
              // TODO 动态规划递推关系
          }
      }
      

    依据以上的递推关系和起始边界, 动态规则算法的代码如下:

    public boolean isMatch(String s, String p) {
        boolean[][] match = new boolean[s.length() + 1][p.length() + 1];
        match[0][0] = true; 
        for(int i = 0; i <= s.length(); i++) {
            for (int j = 1; j <= p.length(); j++) {
                if (p.charAt(j-1) != '*') {
                    match[i][j] = matches(s, i, p, j) && match[i-1][j-1];
                } else {
                    // 当前字符为 * 
                    match[i][j] = match[i][j-2] || // * 的前一个字符匹配0次
                                    (matches(s, i, p, j-1) && match[i-1][j]); // * 的前一个字符匹配1次或者多次
                }
            }
        }
        return match[s.length()][p.length()];
    }
    
    public boolean matches(String s, int i, String  p, int j) {
        if (i == 0) {
            return false;
        }
        // 匹配的条件: (1) 主串和模式串字符相等 (2) 模式串字符为'.', 可以匹配任意字符
        return s.charAt(i-1) == p.charAt(j-1) || p.charAt(j-1) == '.';
    }
    
  • 相关阅读:
    Oracle面试题及答案整理
    Oracle问题总结
    Dubbo(四) -- telnet命令
    Dubbo(三) -- 多协议支持与多注册中心
    每天一算法 -- (冒泡排序)
    Dubbo(二) -- Simple Monitor
    数据库优化
    ActiveMQ内存配置和密码设置
    Dubbo源码导入Eclipse遇到的问题
    Dubbo(一) -- 初体验
  • 原文地址:https://www.cnblogs.com/chenrj97/p/14277871.html
Copyright © 2020-2023  润新知