剑指offer19:正则表达式匹配问题——递归与动态规划两种解法

剑指offer19:正则表达式匹配问题——递归与动态规划两种解法

问题描述

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

分析

每次从字符串里取出一个字符与模式中的字符匹配,如果模式中的字符是‘.’,它可以匹配字符串中的任意字符,如果不是,那么如果它与字符串中的字符相等则匹配。当字符串的字符和模式的字符匹配时,接着匹配后面的字符。
下面,考虑模式中的第二个字符是不是‘*’。如果不是,则可以分为两种情况:

  1. 如果字符串中的第一个字符和模式中的第一个字符相匹配,那么字符串和模式都后移一个字符,然后匹配剩余的字符串和模式;
  2. 如果字符串中的第一个字符和模式中的第一个字符不匹配,则直接返回false

当模式中的第二个字符是‘*’时,又可以分为两种情况方式:

  1. 如果模式中的第一个字符和字符串中的第一个字符不匹配,则在模式上后移两个字符,相当于忽略‘*’和它前面的字符,因为‘*’可以匹配字符串中的0个字符;
  2. 如果模式中的第一个字符和字符串中的第一个字符匹配,则字符串后移一个字符,而在模式上又有两种选择:
    2.1 模式上后移两个字符(例如字符串"ab"和模式"a*ab"匹配到str[0]pattern[0]时,模式后移两个字符,字符串后移一个字符),这一种状态等效于1的情况,可以忽略;
    2.2 模式保持不变(例如字符串"aabbba"和模式"aab*a"匹配到str[2]pattern[2]时,模式保持不变,字符串后移一个字符)。

求解

递归方法

根据上一节的分析很容易写出递归方式的代码:

public static boolean match(char[] str, char[] pattern) {
	if (str == null || pattern == null) {
		return false;
	}
	return matchCore(str, pattern, 0, 0);
}

private static boolean matchCore(char[] str, char[] pattern, int i, int j) {
    //如果模式先遍历完,则一定不匹配;如果同时遍历完,则一定匹配;
	if (j == pattern.length) {
		return i == str.length;
	}
	//注意防止数组索引OutOfBounds;
	if (j < pattern.length - 1 && pattern[j + 1] == '*') {
	        //如果模式下一个为'*',且当前匹配;
			if (i < str.length && (str[i] == pattern[j] || (pattern[j] == '.'))) {
			//模式后移两位,或者模式不变,字符串后移一位;
			return matchCore(str, pattern, i, j + 2) || matchCore(str, pattern, i + 1, j);
		} else {
		    //当前不匹配,模式后移两位;
			return matchCore(str, pattern, i, j + 2);
		}
	}
	//模式下一个不为'*',且当前匹配,则往后匹配;
	if (str.length != i && (str[i] == pattern[j] || pattern[j] == '.')) {
		return matchCore(str, pattern, i + 1, j + 1);
	}
	return false;
}

动态规划

根据前一小节的递归解法,我们可以分析出动态规划解法。首先看递归代码中的matchCore(str, pattern, i + 1, j + 1),其表示整体是否匹配要看下一步是否匹配,递归的过程其实是一种倒序遍历的方式。如果转换成动态规划的思想,并采用顺序遍历的方式,即当前状态是否匹配受上一个状态的影响,上面的代码等效于状态转移方程dp[i][j]=dp[i1][j1]dp[i][j]=dp[i-1][j-1],因此需要建立动态数组boolean dp[str.length+1][pattern.length+1]字符串和模式中的字符都从1开始编号,并赋初始值dp[0][0] == true,表示字符串和模式首端的空字符已匹配
接下来,分别遍历字符串和模式,检验模式的上一个字符是否为‘*’,若是,则:

  1. 如果模式‘*’前一位字符与字符串的上个字符匹配,则跳过该‘*’以及它的前一位字符,或者继续看字符串该字符的前一位字符,即dp[i][j]=dp[i][j2]dp[i1][j]dp[i][j]=dp[i][j-2] || dp[i-1][j];
  2. 如果不匹配,则跳过该‘*’以及它的前一位字符,即dp[i][j]=dp[i][j2]dp[i][j]=dp[i][j-2].

若模式的上一个字符不是‘*’,且与字符串的上个字符匹配,则dp[i][j]=dp[i1][j1]dp[i][j]= dp[i - 1][j - 1].

因此,可以写出如下代码:

public static boolean matchDP(char[] str, char[] pattern) {
	if (str == null || pattern == null) {
		return false;
	}
	boolean[][] dp = new boolean[str.length + 1][pattern.length + 1];
	dp[0][0] = true;
	for (int i = 0; i <= str.length; i++) {
		for (int j = 1; j <= pattern.length; j++) {
			if (j > 1 && pattern[j - 1] == '*') {
				if (i > 0 && (str[i - 1] == pattern[j - 2] || pattern[j - 2] == '.')) {
				    //模式的'*'前一位字符与字符串的上个字符匹配
					dp[i][j] = dp[i][j - 2] || dp[i - 1][j];
				} else {
				    //模式的'*'前一位字符与字符串的上个字符不匹配
					dp[i][j] = dp[i][j - 2];
				}
			} else if (i > 0 && (str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.')) {
			        //模式的前一位字符与字符串的上个字符匹配
					dp[i][j] = dp[i - 1][j - 1];
			}
		}
	}
	return dp[str.length][pattern.length];
}

测试

Main方法及测试用例

public static void main(String[] args) {
		char[] str = { 'a', 'a', 'a' };
		char[] pattern1 = { 'a', '.', 'a' };
		char[] pattern2 = { 'a', 'b', '*', 'a', 'c', '*', 'a' };
		char[] pattern3 = { 'a', 'a', '.', 'a' };
		char[] pattern4 = { 'a', 'b', '*', 'a' };
		//递归方法
		System.out.println(match(str, pattern1));
		System.out.println(match(str, pattern2));
		System.out.println(match(str, pattern3));
		System.out.println(match(str, pattern4));
		//DP方法
        System.out.println(matchDP(str, pattern1));
		System.out.println(matchDP(str, pattern2));
		System.out.println(matchDP(str, pattern3));
		System.out.println(matchDP(str, pattern4));
			
}

输出结果

剑指offer19:正则表达式匹配问题——递归与动态规划两种解法