1.题目
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
1.‘.’ 匹配任意单个字符
2.‘.’ 匹配任意单个字符
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
- 示例1:
输入:s = "aa", p = "a" 输出:false 解释:"a" 无法匹配 "aa" 整个字符串。
- 示例 2:
输入:s = "aa", p = "a*" 输出:true 解释:因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
- 示例3:
输入:s = "ab", p = ".*" 输出:true 解释:".*" 表示可匹配零个或多个('*')任意字符('.')。
- 提示:
- 1 <= s.length <= 20
- 1 <= p.length <= 20
- s 只包含从 a-z 的小写字母。
- p 只包含从 a-z 的小写字母,以及字符 . 和 *。
- 保证每次出现字符 * 时,前面都匹配到有效的字符
2.题解
- 映入脑海的第一个想法是将数字转换为字符串,并检查字符串是否为回文。但是,这需要额外的非常量空间来创建问题描述中所不允许的字符串。
- 第二个想法是将数字本身反转,然后将反转后的数字与原始数字进行比较,如果它们是相同的,那么这个数字就是回文。
- 但是,如果反转后的数字大于 int.MAX\text{int.MAX}int.MAX,我们将遇到整数溢出问题。
- 按照第二个想法,为了避免数字反转可能导致的溢出问题,为什么不考虑只反转 int\text{int}int 数字的一半?毕竟,如果该数字是回文,其后半部分反转后应该与原始数字的前半部分相同。
例如,输入 1221,我们可以将数字 “1221” 的后半部分从 “21” 反转为 “12”,并将其与前半部分 “12” 进行比较,因为二者相同,我们得知数字 1221 是回文。
C# 解法一:分段匹配法
- 根据星号的位置将p切割为多个 尾星串 与 至多一个 无星串,然后从p头到p尾求M值。求解p的某段的M值时,需要根据上一段的M值来依次求解;若上一段M不包含任何值,则匹配失败。若p从头到尾走完了, 则判断最终的M中是否包含了 s.length-1 这个值, 若包含了,则s与p是匹配的。
public class Solution { public bool IsMatch(string s, string p) { List<int> M1 = new List<int>() { -1 }; while (p.Length > 0 && M1.Count > 0) { List<int> M2 = new List<int>() { }; int pStarIndex = p.IndexOf("*"); foreach (int m1 in M1) { string sRight = s.Substring(m1 + 1); if (pStarIndex == -1) { var result = IsMatchNoStar(sRight, p); //无星说明是p的最后一组,若匹配成功则s与p匹配,直接返回true;否则直接continue if (result) return true; continue; } //有星,需要 找到 p[0,starIndex] 与 s 的匹配点 //先判断p[0,starIndex-2]的字符与s的一一对应 int mNow = -1; bool isMatchBeforeStar = true; for (int i = 0; i < pStarIndex - 1; i++) { if (i >= sRight.Length) { isMatchBeforeStar = false; break; } mNow = i; if (!IsMatchChar(sRight[i], p[i])) { isMatchBeforeStar = false; break; } } if (!isMatchBeforeStar) continue; //因为*可以表示0个,所以先将mNow添加到 匹配点集 M2.Add(mNow + (m1 + 1)); mNow++; //再看p[startIndex-1] 与 s的匹配点,找出所有的匹配点 while (mNow < sRight.Length && IsMatchChar(sRight[mNow], p[pStarIndex - 1])) { M2.Add(mNow + (m1 + 1)); mNow++; } } //将startIndex以及之前的串给舍弃,留下startIndex+1以及之后的串 p = p.Substring(pStarIndex + 1); //更新M1 M1 = M2; } return M1.Contains(s.Length - 1); } public bool IsMatchNoStar(string s, string p) { if (s.Length != p.Length) { return false; } for (int i = 0; i < s.Length; i++) { if (!IsMatchChar(s[i], p[i])) { return false; } } return true; } public bool IsMatchChar(char sc, char pc) { return ((pc == '.') || (sc == pc)); } }
可以牺牲部分可读性 来提高效率, 优化后的代码:
public class Solution { public bool IsMatch(string s, string p) { List<int> M1 = new List<int>() { -1 }; List<int> M2 = new List<int>() { }; int pStart = 0; while (p.Length - pStart > 0 && M1.Count > 0) { int pStarIndex = p.IndexOf("*", pStart); foreach (int m1 in M1) { int sStart = m1 + 1; int sRightLen = s.Length - sStart; if (pStarIndex == -1) { //IsMatchNoStar var result = true; if (s.Length - sStart != p.Length - pStart) result = false; else { for (int i = 0; i < s.Length - sStart; i++) { if (!((s[sStart + i] == p[pStart + i]) || (p[pStart + i] == '.'))) { result = false; break; } } } if (result) return true; continue; } int mNow = -1; bool isMatchBeforeStar = true; int pLenBeforeStar = pStarIndex - pStart - 1; for (int i = 0; i < pLenBeforeStar; i++) { if (i >= sRightLen) { isMatchBeforeStar = false; break; } mNow = i; if (!((s[sStart + i] == p[pStart + i]) || (p[pStart + i] == '.'))) { isMatchBeforeStar = false; break; } } if (!isMatchBeforeStar) continue; M2.Add(sStart + mNow++); int pCharIndexBeforeStar = pStarIndex - 1; while (mNow < sRightLen && ((s[sStart + mNow] == p[pCharIndexBeforeStar]) || (p[pCharIndexBeforeStar] == '.'))) M2.Add(sStart + mNow++); } pStart = pStarIndex + 1; var tempMs = M1; M1 = M2; M2 = tempMs; M2.Clear(); } return M1.Contains(s.Length - 1); } }
- 时间复杂度:O( pLen * sLen^2 )
- 最坏的情况下,s全是同一个字母,p是同一个字母加星号,如 s=“aaaaaaa”,p= “aaa*” 。此时外层while以及找星的位置可以看作pLen,内层foreach每次都是对s的遍历,执行次数为sLen,再内层的 for与while加起来 又是对s的遍历,复杂度大概为 O( pLen * sLen^2 )
- 空间复杂度:O( pLen * sLen )
- 我第二层循环里面存在常数数量的变量定义,故为 O(pLen*sLen)
C# 解法二:回溯法
- 回溯法解体的思路与分段匹配法类似,但使用递归后,只需要非常少的代码量。
- 以p为主串,从左向右去匹配s,匹配成功的部分都去掉,也就是说 若最终s与p都变成了空串,则匹配成功。
- 代码结构也很简单,首先是出口,然后是递归分支。
- 出口EXIT:p变成空串时,若s也变成了空串,则匹配成功,否则匹配失败。
- 分支A:p[1]为星号,直接去掉p的前两位,并递归。如 s=“b”,p=“a*b”.
- 分支B:p[1]为星号时,若s第一位与p第一位匹配,去掉s第一位 , 并递归,如 “s=aab”,p=“ab"。否则匹配失败,如 s=“bba”,p="ab”.
- 分支C:p[1]不为星号时,若s与p第一位匹配成功, 则都去掉第一位,并递归,如 s=“aab”,p=“aab*”. 否则匹配失败,如 s=“bab”, p=“aab*” .
- 其中,当p[1]为星号时,分支A与分支B是【或】的关系,只要有一条成功,则匹配成功; 当p[1]不为星号时,就走C。
public class Solution { public bool IsMatch(string s, string p) { //出口,EXIT if (string.IsNullOrEmpty(p)) return string.IsNullOrEmpty(s); //Exit bool first_match = ( !string.IsNullOrEmpty(s) && (p[0] == s[0] || p[0] == '.') ); if (p.Length >= 2 && p[1] == '*') return (IsMatch(s, p.Substring(2)) || // A (first_match && IsMatch(s.Substring(1), p))); //B else return first_match && IsMatch(s.Substring(1), p.Substring(1)); // C } }
可以使用下标指针来代替SubString,从而明显的提高代码效率。 优化后的代码:
public class Solution { public bool IsMatch(string s, string p) { return IsMatch1(s, 0, p, 0); } public bool IsMatch1(string s, int sStart, string p, int pStart) { if (pStart == p.Length) return sStart == s.Length; //Exit bool first_match = ( sStart < s.Length && (p[pStart] == s[sStart] || p[pStart] == '.') ); if (p.Length - pStart >= 2 && p[pStart + 1] == '*') return (IsMatch1(s, sStart, p, pStart + 2) || // A (first_match && IsMatch1(s, sStart + 1, p, pStart))); //B else return first_match && IsMatch1(s, sStart + 1, p, pStart + 1); // C } }
- 时间复杂度:O( (sLen+pLen) 2^(sLen+pLen/2) )*
- 空间复杂度:O( (sLen+pLen) 2^(sLen+pLen/2) )*
C# 解法三:动态规划
public class Solution { public bool IsMatch(string s, string p) { bool[,] dp = new bool[s.Length + 1, p.Length + 1]; dp[s.Length, p.Length] = true; for (int i = s.Length; i >= 0; i--) { for (int j = p.Length - 1; j >= 0; j--) { bool first_match = (i < s.Length && (p[j] == s[i] || p[j] == '.')); if (j + 1 < p.Length && p[j + 1] == '*') { dp[i, j] = dp[i, j + 2] //A || first_match && dp[i + 1, j]; //B } else { dp[i, j] = first_match && dp[i + 1, j + 1]; // C } } } return dp[0, 0]; } }
- 时间复杂度:O(sLen*pLen)
- 最table中每个值会被计算一次,不会重复计算,而每个格子的计算时间可认为是O(1),所以总时间复杂度为O(sLen*pLen)
- 空间复杂度:O(sLen*pLen)
- able空间复杂度为 O(sLen*pLen).