正则表达式匹配问题的递归和动态规划解法

本文探讨了正则表达式的递归与动态规划解法,包括'.‘和’*‘的匹配规则,详细解析了递归算法的实现过程及动态规划的效率优势。

正则表达式匹配问题的递归和动态规划解法

题目

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

递归解法

使用递归来实现每一步的比较。看模式字符串中当前位置的下一个字符是否是“*”。

  1. 模式中当前位置下一个字符是“*”时:若当前字符不相等,则模式后移两个字符,继续比较;若当前字符相等,则有三种情况:(比如a 和 a*进行匹配,三种情况使用“||”进行并列比较)
    • 字符串字符位置不变,模式后移两个字符,继续比较;//表示a*被忽略
    • 字符串后移一个字符,模式后移两个字符,继续比较;//表示a 和 a*匹配
    • 字符串后移一个字符,模式字符位置不变,继续比较。//表示a 和 a*匹配,但是星号表示不止一个a。
  2. 当模式中当前位置下一个字符不为“*”时:若当前字符相等,则字符串和模式都后移一个字符,继续调用函数进行比较;若不相等,则返回false。

可以看出上述思路中关键是对 “*” 的判断,对于a和a*的匹配有三种不同的情况,所以需要分开考虑,对应了星号前面的字符出现了多少次,三种情况用逻辑或并列比较,也就意味着三种情况中有一种满足就算此次匹配成功。

class Solution {
    public boolean isMatch(String s, String p) {
        // 边界条件
        if (s == null || p == null) {
            return false;
        }
        return isMatchCore(s, 0, p, 0);
    }

    private boolean isMatchCore(String s, int sIndex, String p, int pIndex) {
        // 字符串和模式都递归到最后一个字符的右边了,表示完全匹配
        if (sIndex == s.length() && pIndex == p.length()) {
            return true;
        }

        // 模式不可以先结束,模式先结束肯定是不匹配的,但是字符串如果先结束则不一定,
        // 比如:a和aa*,字符串中的指针先走到length的位置,但此时并没有结束
        // 自己做的时候没考虑好,所以考虑问题一定要全面
        if (sIndex < s.length() && pIndex == p.length()) {
            return false;
        }

        // 程序的主体是对模式判断下一个字符是否为“*”,
        // 模式中的指针最多到倒数第二个。
        if (pIndex+1 < p.length() && p.charAt(pIndex+1) == '*') {
            if (sIndex < s.length() && (s.charAt(sIndex) == p.charAt(pIndex) || p.charAt(pIndex) == '.')) {
                // 三种情况的含义一定要明确,参考上面的思路
                return isMatchCore(s, sIndex, p, pIndex+2) 
                || isMatchCore(s, sIndex+1, p, pIndex+2)
                || isMatchCore(s, sIndex+1, p, pIndex);
            }else {
                // 如果当前位置s和p中的字符不相等,则模式中跳过,原字符串中不要跳过,因为还没匹配上
                return isMatchCore(s, sIndex, p, pIndex+2);
            }          
        }
		// 模式的下一个字符不是“*”,则直接比较字符串和模式中当前位置的字符
        if (sIndex < s.length() && (s.charAt(sIndex) == p.charAt(pIndex) || p.charAt(pIndex) == '.')) {
            return isMatchCore(s, sIndex+1, p, pIndex+1);
        }
        return false;
    }
}

递归解法在LeetCode中耗时2000多ms,上述类似a和a*这样的情况越短,递归的次数就越多,时间也就越慢。最近在学习动态规划,发现题解中有大神给出了动态规划的解法(链接在此:Jerry的题解),故学习一波。

动态规划解法

运用动态规划,首先明确数组含义。假设原字符串为A1…m,模式串为B1…n,dp[i][j]表示A1…i和B1…j是否匹配,匹配为true,不匹配为false。

如何划分子问题?判断A1…i和B1…j是否匹配需要根据A1…i-1和B1…j-1来进行具体判断。见下面的状态转移方程。

状态转移方程:

  • 第一种情况:如果Bj是普通字符,判断Bj和Ai是否相等即可;如果Bj是 “.”,表示可以匹配任意字符。这两种情况下有如下转移方程:dp[i][j] = dp[i-1][j-1],我们在写这个转移方程之前有个前提条件,就是i和j位置处的字符必须匹配,否则就没有状态转移这一说,直接就是false了。
  • 第二种情况:Bj= “*”,对这个星号的处理又可以分为两种情况:
    • 直接忽略,比如a和c*是不匹配的(这里的c是Bj的前一个字符),那我们直接把c*忽略掉好了,此时星号就表示c出现了0次。转移方程:dp[i][j] = dp[i][j-2]
    • 匹配上了,比如a和a*就匹配上了,此时转移方程:dp[i][j] = dp[i-1][j]

边界条件:

  • 空串和空模式是匹配的,即dp[0][0]=true
  • 空串和非空模式,需要计算得出,比如“”和“a*”是匹配的。
  • 非空串和空模式必不匹配,即f[1][0]=...=f[n][0]=false
  • 非空串和非空模式,也是需要计算的。

动态规划代码如下,LeetCode运行2ms!

class Solution {
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();
        boolean[][] dp = new boolean[m+1][n+1];

        // 对原字符串和模式串进行遍历匹配
        // 注意:i和j的意思指的是长度,i=1表示在原字符串第一个字符的位置
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                // 判断模式是否为空
                if (j == 0) {
                    dp[i][j] = i == 0;// 参考dp的边界条件
                }else {
                    // 判断模式中当前字符是否为 * 
                    if (p.charAt(j-1) != '*') {
                        if (i > 0 && (s.charAt(i-1)==p.charAt(j-1) 
                        || p.charAt(j-1)=='.')) {
                            dp[i][j] = dp[i-1][j-1];
                        }
                    }else {
                    	// 下面两种情况中的 “|=” 表示两种情况都有可能,
                    	// 只要有一个能匹配上就dp[i][j]就是true
                        // 第一种情况:直接忽略
                        if (j >= 2) {
                            dp[i][j] |= dp[i][j-2];
                        }
                        // 第二种情况:星号前面的字符匹配上了
                        if (i>=1 && j>=2 && (s.charAt(i-1)==p.charAt(j-2) 
                        || p.charAt(j-2)=='.')) {
                            dp[i][j] |= dp[i-1][j];
                        }
                    }
                }
            }
        }
        return dp[m][n];
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值