正则表达式匹配算法
在leetcode玩耍遇到了这么一个题给定一个字符串 (s) 和一个字符模式 (p)。实现支持 '.'和'*'的正则表达式匹配。
'.' 匹配任意单个字符。
'*' 匹配零个或多个前面的元素。
匹配应该覆盖整个字符串 (s) ,而不是部分字符串。
说明:
s可能为空,且只包含从a-z的小写字母。
p可能为空,且只包含从a-z的小写字母,以及字符.和*。
示例 1:输入: s = "aa" p = "a" 输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:输入: s = "aa" p = "a*" 输出: true
解释: '*' 代表可匹配零个或多个前面的元素, 即可以匹配 'a' 。因此, 重复 'a' 一次, 字符串可变为 "aa"。
示例 3:输入: s = "ab" p = ".*" 输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:输入: s = "aab" p = "c*a*b" 输出: true
解释: 'c' 可以不被重复, 'a' 可以被重复一次。因此可以匹配字符串 "aab"。 示例 5:输入: s = "mississippi" p = "mis*is*p*." 输出: false
本人当时想了很久毫无头绪,后来在百度上参照了几篇介绍正则表达式算法的文章才解决了问题,把我的见解分享给大家
解决这道题要从状态机说起
自动机也称状态机。下面的自动机对应正则表达式a(bb)+a。
自动机总是处于一个状态之中,也就是图中的圈圈。在读入字符串的时候,它从一个状态进入另一个状态。自动机有两个特殊的状态:开始开始状态和匹配状态。开始状态始于一个但箭头,匹配状态止于一个双环。
自动机一次读入一个字符串,按照箭头转换状态。假设输入字符串abbbba。当字符串读入第一个字符a,此时的状态为s1。它耕者箭头a到达状态s1。重复过程,直到到达状态s4。
自动机结束于s4,这是一个匹配成功的状态,也就说匹配成功了,如果自动机结束于非匹配成功状态,那么匹配失败。如果在运行过程中,没有办法到达其他状态,那么自动机提前结束。
我们刚才考虑的是确定状态自动机,因为在每一个状态,可达的下一个状态至多只有一个。我也可以创建一个有多种选择的机器。以之前的自动机为例:
机器的状态是不确定的,因为当它读入ab到达s2,它对于下一个状态有多个选择:它可以回到s1或者到达s3。由于机器无法预料接下来的输入,它不知道该选择哪个状态。可以同时保存多种状态的也就是非确定状态自动机(NFA)。
正则表达式转换成NFA
在原题中只有".", "*"两个正则表达式匹配,我们做一个简单的例子支持这两个字符
java代码如下:
/*** NFA节点类** @author Julius*/
static class Node {
/*** 当前节点的值*/
char value;
/*** 当前节点的下一个节点*/
Node node;
/*** 当前节点的下一个状态节点集合*/
List linkNodes;
/*** 标记为头*/
boolean isHead;
/*** 标记为尾*/
boolean isTail;
public Node(char value) {
super();
this.value = value;
this.linkNodes = new ArrayList<>();
}
}
/*** 在这里将正则表达式转换成NFA** @param s* 待转换的正则表达式* @return NFA模型*/
public static List parse2Nodes(String s) {
CopyOnWriteArrayList nodes = new CopyOnWriteArrayList<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
nodes.add(new Node(c));
}
for (int i = 0; i < nodes.size(); i++) {
Node n = nodes.get(i);
/** 节点的值是普通字符时,上个节点指向本结点*/
if (n.value >= 'a' && n.value <= 'z') {
Node pn = getPreNode(nodes, i);
if (pn != null)
pn.node = n;
}
/** 节点的值是匹配任意单个字符(.)时,上个节点指向本结点*/
if (String.valueOf(n.value).equals(".")) {
Node pn = getPreNode(nodes, i);
if (pn != null)
pn.node = n;
}
/** 节点的值是匹配零个或多个前面的元素时,上一个节点增加一个指向自己的状态* ,增加一个指向下一个节点的状态并删除本节点*/
if (String.valueOf(n.value).equals("*")) {
Node pn = getPreNode(nodes, i);
Node nn = getNextNode(nodes, i);
if (pn != null) {
pn.linkNodes.add(pn);
}
if (nn != null) {
pn.linkNodes.add(nn);
}
nodes.remove(n);
i--;
}
}
nodes.get(0).isHead = true;
nodes.get(nodes.size() - 1).isTail = true;
return nodes;
}
/*** 获取前一个节点** @param nodes* @param index* @return*/
public static Node getPreNode(List nodes, int index) {
if (index - 1 >= 0) {
return nodes.get(index - 1);
}
return null;
}
/*** 获取下一个节点** @param nodes* @param index* @return*/
public static Node getNextNode(List nodes, int index) {
if (index + 1 <= nodes.size() - 1) {
return nodes.get(index + 1);
}
return null;
}
比如c*a*b*这个正则表达式,经过NFA算法可到如下的模型
图中的圆圈表示一个状态,对应Node节点类,黑色箭头与蓝色箭头表示状态的转换方向
NFA匹配算法
现在我们有了匹配正则表达式的方法:把正则表达式转换为NFA,然后把字符串作为输入来运行NFA。现在来看一下模拟NFA运行的算法
java代码如下:
/*** 检测字符串匹配正则表达式** @param s* 匹配字符串* @param p* 正则表达式* @return*/
public static boolean isMatch(String s, String p) {
List nodes = parse2Nodes(p);
return isMatch(s, 0, nodes.get(0));
}
/*** 深度优先匹配字符串** @param s* 带匹配字符串* @param index* 字符下标* @param node* NFA模型* @return*/
public static boolean isMatch(String s, final int index, Node node) {
if (index > s.length() - 1) {
return false;
}
char c = s.charAt(index);
/** 找到匹配的字符,匹配成功*/
if (c == node.value || node.value == '.') {
/** 字符指针移动到最后一位,状态机到达最终状态返回匹配成功*/
if (s.length() - 1 == index && node.isTail) {
return true;
}
/** 转换到下一个状态尝试匹配*/
if (node.node != null) {
if (isMatch(s, index + 1, node.node)) {
return true;
}
}
for (Node ln : node.linkNodes) {
if (isMatch(s, index + 1, ln)) {
return true;
}
}
} else {
/** 转换到下一个状态尝试匹配*/
for (Node ln : node.linkNodes) {
if (ln != node) {
if (isMatch(s, index, ln)) {
return true;
}
}
}
}
/** 不满足匹配成功条件,回溯*/
return false;
}
主函数中调用方法isMatch(String s, String p)即可检测字符串是否匹配表达式,例如isMatch("ab", ".*")返回true,表示".*"可以匹配字符串
以下是匹配测试输入: aaa, p=a
结果: 不匹配
输入: s=aa, p=a*
结果: 匹配
输入: s=aab, p=c*a*b*
结果: 匹配
输入: s=aab, p=ca*b*
结果: 不匹配
输入: s=ab, p=.*
结果: 匹配
输入: mississippi, p=mis*is*p*.
结果: 不匹配
输入: abcdef, p=.*de.
结果: 匹配
输入: abcdef, p=.*p*de.
结果: 匹配
输入: abcdef, p=.*p*d.e.
结果: 不匹配
至此我们的正则表达式算法已完成
结尾
本篇介绍了正则表达式匹配的常见算法非确定型有限状态自动机(NFA),并贴上代码实现了算法,希望给想了解正则表达式和想实现正则表达式的人起到帮助作用