Day52:正则表达式匹配

剑指Offer_编程题——正则表达式匹配

题目描述:

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

具体要求:

时间限制: C/C++ 1秒,其他语言2秒
空间限制: C/C++32M,其他语言64M

具体实现:

一、背景知识介绍:
1、正则表达式:
   在解题之前,我们先进行正则匹配的介绍,在维基百科中是这样介绍正则表达式的。正规表达式(Regular Expression)又称正规表示式、正规表示法、规则表达式、常规表示法,是计算机科学的一个概念。正规表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正规表达式通常被用来检索、替换那些符合某个模式的文本。正则表达式可以用形式化语言理论的方式来表达。正则表达式由常量和算子组成,它们分别表示字符串的集合和在这些集合上的运算。为了避免括号,假定Kleene星号有最高优先级,接着是串接,接着是并集。如果没有歧义则可以省略括号。以下是正则表达式的表
正则表达式速查表
2、动态规划
  在解题过程中会用到动态规划的思想,因此我们首先给大家介绍动态规划的知识。在维基百科中,动态规划是这样介绍的。动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
  在实际应用动态规划中,首先是拆分问题,我的理解就是根据问题的可能性把问题划分成一步一步这样就可以通过递推或者递归来实现.关键就是这个步骤,动态规划有一类问题就是从后往前推到,有时候我们很容易知道:如果只有一种情况时,最佳的选择应该怎么做.然后根据这个最佳选择往前一步推导,得到前一步的最佳选择。然后就是定义问题状态和状态之间的关系,我的理解是前面拆分的步骤之间的关系,用一种量化的形式表现出来,类似于高中学的推导公式,因为这种式子很容易用程序写出来,也可以说对程序比较亲和(也就是最后所说的状态转移方程式)。我们再来看定义的下面的两段,我的理解是比如我们找到最优解,我们应该讲最优解保存下来,为了往前推导时能够使用前一步的最优解,在这个过程中难免有一些相比于最优解差的解,此时我们应该放弃,只保存最优解,这样我们每一次都把最优解保存了下来,大大降低了时间复杂度。动态规划最常用的问题就是经典的数字三角形问题。
二、实现思路:
思路一:
  根据我们前面给大家介绍的正则表达式的相关知识以及相应的动态规划的相关应用场所。这道题可以用动态规划的思路来解决该题。在这里需要我们注意的是:应该注意到,’.’ 是用来当做一个任意字符,而 ‘’ 是用来重复前面的字符。这两个的作用不同,不能把 ‘.’ 的作用和 ‘’ 进行类比,从而把它当成重复前面字符一次。另外还需要注意的是:’ '也是匹配成功的情况。具体动态规划体现如下:
  当S为空,P为空或者"xy"形式的可以匹配
  当P为空,S为空可以匹配
  循环扫描字符串匹配情况并记录在 dp[][] 数组
  如果 str[i-1] == pattern[j-1] || pattern[j-1] == ‘.’, 此时dp[ i ][ j ] = dp[i-1][j-1],说明字符匹配
  如果 pattern[j] ==‘
1、 如果 str[i-1] == pattern[j-2] || pattern[ j-2 ] == ‘.’
此时dp[i][j] = dp[i][j-2] // a
匹配0次,此时""代表空串
或者 dp[i][j] = dp[i][j-1] // a
匹配1次,
或者 dp[i][j] = dp[i-1][j] // a匹配多次
2、 如果str[i-1] != pattern[j-2] , 此时dp[i][j] = dp[i][j-2] //a
匹配0次
  具体我们用java实现如下:

public class Solution{
	public boolean match(char[] str, char[] pattern){
		int m = str.length, n = pattern.length;
		boolean[][] dp = new boolean[m + 1][n + 1];
		dp[0][0] = true;
		for(int i = 1; i <= n; i++){
			if(pattern[i - 1] == '*')
				dp[0][i] = dp[0][i - 2];
		}
		for(int i = 1; i <= m; i++){
			for(int j = 1; j <= n; j++){
				if(str[i - 1] == pattern[j - 1] || pattern[j - 1] == '.')
					dp[i][j] = dp[i - 1][j - 1];
				else if(pattern[j - 1] == '*'){
					if(pattern[j - 2] == str[i - 1] || pattern[j - 2] == '.'){
						dp[i][j] |= dp[i][j - 1];
						dp[i][j] |= dp[i - 1][j];
						dp[i][j] |= dp[i][j - 2];
					}
					else
						dp[i][j] |= dp[i][j - 2];
				}
			}
		}
			return dp[m][n];
	}
}

代码效果图如图所示:
代码通过示意图
  代码实现需要我们注意两点:①:动态数组,字符串和模式中的字符都从1开始编号。②在判断str是否和前一个字符相等的时候,我们用到了 |= 而不是 != 。这两种运算符有着本质的区别。!=是一种判断运算符,而|=是先按照二进制进行进位,然后再进行赋值,这里需要我们注意。
思路二:
   除了上述我们提到的动态规划以外,还可以进行递归,具体过程如下:
  每次分别在str 和pattern中取一个字符进行匹配,如果匹配,则匹配下一个字符,否则,返回不匹配。设匹配递归函数 match(str, pattern)。如果模式匹配字符的下一个字符是‘
’: 如果pttern当前字符和str的当前字符匹配:有以下三种可能情况
  (1)pttern当前字符能匹配 str 中的 0 个字符:match(str, pattern+2)
  (2)pttern当前字符能匹配 str 中的 1 个字符:match(str+1, pattern+2)
  (3)pttern当前字符能匹配 str 中的 多 个字符:match(str+1, pattern)
   如果pttern当前字符和和str的当前字符不匹配pttern当前字符能匹配 str 中的 0 个字符:(str, pattern+2),如果模式匹配字符的下一个字符不是‘’,进行逐字符匹配。对于 ‘.’ 的情况比较简单,’.’ 和一个字符匹配 match(str+1, pattern+1)
另外需要注意的是:空字符串”” 和 “.
” 是匹配的。具体我们用java和python实现以上思路:
1、我们用java将其实现

public class Solution {
    public boolean match(char[] str, char[] pattern)
    {
              if(str==null||pattern==null) return false;
        
		return matchCore(str,0,pattern,0);
    }
	
	public boolean matchCore(char[] str,int s, char[] pattern,int p) {
		if(str.length<=s&&pattern.length<=p)
			return true;//都匹配完了
		if(str.length>s&&pattern.length<=p)
			return false;//模式完了,字符串还有
		//模式串a*a没结束,匹配串可结束可不结束
		
		if(p+1<pattern.length&&pattern[p+1]=='*'){//当前pattern的下一个是*号时
			
			//字符串完了
			if(s>=str.length) return matchCore(str, s, pattern, p+2);
			else{
			
			if(pattern[p]==str[s]||pattern[p]=='.'){
				//当前位置匹配完成,移动到下一个模式串
				return matchCore(str,s+1, pattern,p+2)
						||matchCore(str,s+1, pattern,p)
						||matchCore(str,s, pattern,p+2);
			}else
				return matchCore(str, s, pattern, p+2);
			}
		}
		//当前pattern的下一个不是*时候
		if(s>=str.length) return false;
		else{
		if(str[s]==pattern[p]||pattern[p]=='.')
			return matchCore(str, s+1, pattern, p+1);
		}
		return false;
    }
}

代码效果图如图所示:
代码通过示意图
2、我们用python将其实现:

class Solution:
	def match(self, s, pattern):
		if not s and not pattern:
			return True
		if s and not pattern:
			return False
		if len(pattern) > 1 and pattern[1] == '*':
			if (len(s) > 0 and (s[0] == pattern[0] or pattern[0] == '.')):
				return (self.match(s, pattern[2:]) or self.match(s[1:], pattern[2:]) or self.match(s[1:], pattern))
			else:
				return self.match(s, pattern[2:])
		if len(s) > 0 and (s[0] == pattern[0] or pattern[0] == '.'):
			return self.match(s[1:], pattern[1:]);
		return False

代码效果图如图所示:
代码通过示意图

总结

  本题是主要通过正则表达式来考察我们对实际问题分析的能力以及对动态规划和递归算法的理解和掌握。本文在做题之前首先给大家介绍了正则匹配以及动态规划的相关知识。另外我们通过递归和动态规划算法来实现该题,并且在递归的时候,我们用到了python和java两种语言将其实现。这里需要我们注意的是:动态数组,字符串和模式中的字符都从1开始编号。因此,我们在做题的时候,应该多次尝试各种方法,扩展自己的思维,写出优质的代码。总之,我们要继续加油,争取早日找到工作,Good Luck!!!

参考文献

[1] 正则表达式
[2] 经典中的经典算法:动态规划(详细解释,从入门到实践,逐步讲解)
[3] hll174
[4] 送外卖最快的程序猫
[5] weixin_43994113

猜你喜欢

转载自blog.csdn.net/Oliverfly1/article/details/106872063