AI工具人
提示词工程师

用正则表达式匹配3的任意倍数

正则表达式能匹配3的任意倍数?(注意是任意倍数) ,我曾经也很震惊,但确实可以。我5年多前练习正则表达式,在Regex Golf这个正则表达式测试网站上发现了这个题,当时完全没有任何头绪,于是我在知乎提问正则表达式如何匹配 3 的倍数 ,但是得到了好多知乎大佬的关注,也上了当天的热榜。 排名第一的答主已经给出了答案和思路,但这么多年来我一直都没看懂,最近学习编译原理,看到正则表达式和DFA,于是仔细研究了一下这个问题,并将问题扩展至匹配N的倍数,最后给出通用解法和代码。

^(([0369]|[147][0369]*[258])|([258]|[147][0369]*[147])([0369]|[258][0369]*[147])*([147]|[258][0369]*[258]))*$  

先给出答案,上面这个正则表达式确实能够匹配任意3的着倍数,再次强调是任意,它确实能匹配任意长度的3的倍数(严谨一点应该是正整数倍,这里不再细究)。 这个正则表达式是这么来的?实际上它是由下面这个DFA(确定性有穷状态机)生成的。
在这里插入图片描述

构造DFA

那这个DFA又是如何来的?首先我们来解释下何为DFA(Deterministic finite automata),首先DFA是一种状态机(automata),状态机是用来描述状态在不同条件下相互转换的关系,DFA就是在指定条件下,这个状态转换路径是确定且唯一的,这也是它和NFA(Nondeterministic finite automata)的区别。 在正则表达式中,DFA就是在给定某个字符的情况状态如何转移,一般情况下有个初始状态和终止状态,如上图所示状态A就是终止状态,一般用双层环表示,上图并为标识初始状态,其实这里初始状态也是A。在正则表达式对应的DFA中如果当前状态是终止状态,说明正则表达式匹配成功。

如果我们要生成一个匹配N的倍数的DFA,我们的思路是这样的,如果一个数X是N的倍数,那么一定是X % N == 0,这也是我们用来判断X是不是N的倍数方法,我们是把X看成是一个数字一个整体。其实我们这里也是通过取mod是否为零来做的,但我们需要对取mod的过程稍微改造下,举例如下:

0128%3 == (((((0 % 3)*10 + 1) % 3)*10 + 2)*10 + 8) % 3 

左右两边的表达式计算结果是等价的,只是我们右边是把每位拆开来单独计算。仔细看看右边的表达式,我们从高位到低位挨个计算mod 3的结果,然后把计算结果乘以10再加到下一位上继续计算,直到后面没有其他数字为止。这种从前到后按位去mod的方式就和正则表达式从前到后按字符去匹配的方式一致了,我们可以按当前状态和新到的数字去计算下一个状态是啥了。

理解了这点,我们就可以开始构造我们的DFA了,我们可以以mod N之后余几做作为状态,比如N为3时,总共有0 1 2 三种状态。每增加一个数字,一个状态都可以转移到了一个状态,例如:状态$S_k$新增数字m的情况下可以转移到状态$S_{(k*10 + m) \% 3}$,接下来我们只需要建立每个状态在每种不同输入下状态转移的关系就行了,伪代码如下。

for i = 0 to N:
    for j = 0 to N:
        if i*10 + k) % n == :
            建立一条状态Si到Sj的边 

将上面建立好的关系绘制成图就得到了如下DFA。
在这里插入图片描述

DFA推导出正则表达式

对于上文中匹配3的倍数的DFA,因为状态还算比较少,我们可以人肉推导出来。从上图我们可以看出ABC三个状态是相互依存的关系,我们可以把这种关系列成三个方程式。

A = A[0369] | B[258] | C[147] 
B = A[147] | B[0369] | C[258]
C = A[258] | B[147] | C[0369]

根据Arden定理 若$X = XA | B$则$X = BA*$ ,我们可以再做如下转化。

A = (| B[258] | C[147])[0369]* (1)
B = (A[147] | C[258])[0369]* (2)
C = (A[258] | B[147])[0369]* (3)

为了让你理解如何计算出A这里我做一个不恰当但很合理的转化,你可以把连接和 | 分别看出四则运算里的乘和加,把ABC分别看成三个未知数,然后我们就得到了一个三元一次方程组,而我们只需要求解出A,求解过程如下:

把(3)带入(1)(2)分别得到:

A = (| B[258] | (A[258] | B[147])[0369]*[147])[0369]* (4)
B = (A[147] | (A[258] | B[147])[0369]*[258])[0369]* (5)  

用分配律展开 (5) 中的竖线得到

B = A[147][0369]* | A[258][0369]*[258][0369]* | B[147][0369]*[258][0369]*

这里再用一次Arden定理 得到

B = A[147][0369]*([147][0369]*[258][0369]*)* 
  | A[258][0369]*[258][0369]*([147][0369]*[258][0369]*)* (6)

把(6)带入到(4)中,并继续用Arden定理 消去右侧的A

A = (| B[258] | (A[258] | B[147])[0369]*[147])[0369]*
  =   [0369]* 
    | B[258][0369]*
    | A[258][0369]*[147][0369]*
    | B[147][0369]*[147][0369]* 
  =   [0369]* 
    | A[147][0369]*([147][0369]*[258][0369]*)*[258][0369]*
    | A[258][0369]*[258][0369]*([147][0369]*[258][0369]*)*[258][0369]*
    | A[258][0369]*[147][0369]*
    | A[147][0369]*([147][0369]*[258][0369]*)*[147][0369]*[147][0369]* 
    | A[258][0369]*[258][0369]*([147][0369]*[258][0369]*)*[147][0369]*[147][0369]* 
  = [0369]* (
                  [147][0369]*([147][0369]*[258][0369]*)*[258][0369]*
    | [258][0369]*[258][0369]*([147][0369]*[258][0369]*)*[258][0369]*
    |             [147][0369]*([147][0369]*[258][0369]*)*[147][0369]*[147][0369]* 
    | [258][0369]*[258][0369]*([147][0369]*[258][0369]*)*[147][0369]*[147][0369]*
    | [258][0369]*[147][0369]* )*
  = [0369]* (
      (
        [147][0369]*
      | [258][0369]*[258][0369]*
      ) ([147][0369]*[258][0369]*)* (
        [258][0369]*

      | [147][0369]*[147][0369]*
      )
    | [258][0369]*[147][0369]* )*

加上^ 和 $ 就得到了能够匹配3的倍数的正则表达式,推导过程很艰辛,有没有什么方法可以自动把DFA转为正则表达式?

你可能注意到这个正则表达式和我在文章开头给出的不一样,但这个正则表达式也是正确的。这个正则表达式我自己实在是没推导出来,所以推导过程引用了知乎的内容,但我找到了能够将任意DFA转成正则表达式的方法,文章开头的正则表达式就是我用代码自动生成的,接下来就教你DFA如何自动转正则表达式。

任意DFA转正则表达式的方法

DFA转Regex的核心思想也很简单,逐个删除中间状态(非初始状态和终止状态),删除过程中把经过这个状态的路径合并到其他路径上,举例如下:
在这里插入图片描述
我们删除q时,需要对经过状态q的路径做合并。(· 表示连接符,star()表示Kleene star,其实就是正则表达式中的星号,表示出现0次或任意多次)

L[p->r] = L[p-q] · star[L[q->q]] · L[q-r]
L[r->p] = L[r-q] · star[L[q->q]] · L[q-p]   

经装换后得到了如下DFA。
在这里插入图片描述
如果p为初始状态,r为终止状态,我们可以直接把这个DFA通过如下公式转为正则表达式。
star(L[s,s]) · L[s,f] · star(L[f,s] · star(L[s,s]) · L[s,f] + L[f,f])
为了加深理解,我们再举个例子。
在这里插入图片描述
在删除完状态2之后,1->3的路径需要并上经过状态2的路径,也就是1->2->3。 同理 3->1的路径需要并上3->2->1,最后DFA变成如下。
在这里插入图片描述
用同样的方式删除完状态3之后,我们只剩下状态1,因为状态1即是初始状态,又是终止状态,所以我们要的正则表达式就是0->0的路径。在只剩下初始状态$State_s$和终止状态$State_e$的DFA,$State_s$->$State_e$的路径就可以代表整个原始DFA,这个路径也可以当成正则表达式直接使用。
在这里插入图片描述
最后得到了(??+(?+??)(??)*(?+??))*,把+ 替换为 |,并把ab分别替换成状态转移条件就变成一个可用的正则表达式。
在给出完整代码前,我先给出DFA转Regex的伪代码:

# 首先需要把两个状态间的多条边合并成1条
for i = 1 to n:
  for j = 1 to n:
    if i == j then:
      L[i,j] := ε
    else:
      L[i,j] := ∅
    for a in Σ:
      if trans(i, a, j):
        L[i,j] := L[i,j] + a 
remove(k):
  for i = 1 to n:
    for j = 1 to n:
      L[i,j] += L[i,k] . star(L[k,k]) . L[k,j]

# 逐个删除中间状态  
for i = 1 to n:  
  if not(final(i)) and not(initial(i)):  
    remove(i)

我用Java实现了N任意倍数的DFA生成,并实现了DFA转Regex的功能,完整代码如下。调用getDFA(3)返回的就是绘制成图就是上文中出现多次的DFA,这里我用了HashMap存储各个状态之间的关系。

代码

public class DFA2regex {
    private final static int INITSTATE = 0;
    private final static int FINALSTATE = 0;
    private final static Set<Integer> set = new HashSet<>();

    // 生成DFA时我直接将多条边合并成了一条 
    private static Map<String, String> getDFA(int n) {
        Map<String, List<String>> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                for (int k = 0; k < 10; k++) {
                    String key = key(i, j);
                    if (!map.containsKey(key)) {
                        map.put(key, new LinkedList<>());
                    }
                    if ((i*10 + k) % n == j) {
                        List<String> list = map.get(key);
                        list.add(String.valueOf(k));
                    }
                }
            }
        }
        Map<String, String> finalRes = new HashMap<>();
        for (HashMap.Entry<String, List<String>> entry : map.entrySet()) {
            String key = entry.getKey();
            String val =  entry.getValue().stream().reduce("", String::concat);
            if (val.length() > 1) {
                val = "[" + val + "]";
            }
            finalRes.put(key, val);
        }
        return finalRes;
    }

    private static void remove(int k, int n, Map<String, String> dfa) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (set.contains(i) || set.contains(j)) {
                    continue;
                }
                dfa.put(key(i, j), concat(dfa.get(key(i, j)), dfa.get(key(i, k)), star(dfa.get(key(k, k))), dfa.get(key(k, j))));
            }
        }
    }

    private static String concat(String s1, String s2, String s3, String s4) {
        String str1 = s1;
        String str2 = s2 + s3 + s4;
        if (str1.length() > 0 && str2.length() > 0) {
            str1 += "|";
        }
        String res = str1 + str2;
        if (res.contains("|") || res.contains("*")) {
            return "(" + res + ")";
        }
        return res;
    }


    private static String star(String s) {
        if (s.equals("") || s.equals("|")) {
            return "";
        }
        return s + "*";
    }

    private static String key(int s, int e) {
        return s + "_" + e;
    }

    public static void main(String[] args) {
        int n = 4;
        Map<String, String> dfa = getDFA(n);
        for (int i = 0; i < n; i++) {
            if (INITSTATE != i && FINALSTATE != i) {
                set.add(i);
                remove(i, n, dfa);
            }
        }
        System.out.println("^" + star(dfa.get("0_0")) + "$");
    }
}

写上面代码的过程中我踩到几个坑,正则表达式各运算符是有优先级的,所以需要再状态消除过程中对中间表达式左右添加 () ,为了让生成的正则表达式简洁,我在concat()中做了一些特殊的处理,让最终结果没有多余的小括号| 符号。

彩蛋

这里分别列一下能匹配1-6的任意倍数的正则表达式。为什么不列更多,因为后面生成的正则表达式已经越来越长了,列不下了,7的就已经几千个字符了,有兴趣大家可以自己跑下上面代码生成下。事实上因为正则表达式越来越长,数字越大越耗资源,我自己电脑跑16就跑不出结果了。

1

^[0123456789]*$

2

^([02468]|[13579][13579]*[02468])*$

3

^(([0369]|[147][0369]*[258])|([258]|[147][0369]*[147])([0369]|[258][0369]*[147])*([147]|[258][0369]*[258]))*$

4

^((([048]|[159][37]*[26])|([26]|[159][37]*[048])([26]|[159][37]*[048])*([048]|[159][37]*[26]))|(([37]|[159][37]*[159])|([26]|[159][37]*[048])([26]|[159][37]*[048])*([37]|[159][37]*[159]))(([159]|[37][37]*[159])|([048]|[37][37]*[048])([26]|[159][37]*[048])*([37]|[159][37]*[159]))*(([26]|[37][37]*[26])|([048]|[37][37]*[048])([26]|[159][37]*[048])*([048]|[159][37]*[26])))*$

5

^(((([05]|[16][16]*[05])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([05]|[16][16]*[05]))|(([38]|[16][16]*[38])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([38]|[16][16]*[38]))(([38]|[16][16]*[38])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([38]|[16][16]*[38]))*(([05]|[16][16]*[05])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([05]|[16][16]*[05])))|((([49]|[16][16]*[49])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([49]|[16][16]*[49]))|(([38]|[16][16]*[38])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([38]|[16][16]*[38]))(([38]|[16][16]*[38])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([38]|[16][16]*[38]))*(([49]|[16][16]*[49])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([49]|[16][16]*[49])))((([49]|[16][16]*[49])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([49]|[16][16]*[49]))|(([38]|[16][16]*[38])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([38]|[16][16]*[38]))(([38]|[16][16]*[38])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([38]|[16][16]*[38]))*(([49]|[16][16]*[49])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([49]|[16][16]*[49])))*((([05]|[16][16]*[05])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([05]|[16][16]*[05]))|(([38]|[16][16]*[38])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([38]|[16][16]*[38]))(([38]|[16][16]*[38])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([38]|[16][16]*[38]))*(([05]|[16][16]*[05])|([27]|[16][16]*[27])([27]|[16][16]*[27])*([05]|[16][16]*[05]))))*$

6

^((((([06]|[17][39]*[28])|([28]|[17][39]*4)([06]|5[39]*4)*(4|5[39]*[28]))|(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*(([06]|[17][39]*[28])|([28]|[17][39]*4)([06]|5[39]*4)*(4|5[39]*[28])))|(((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06]))|(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06])))((([06]|[39][39]*[06])|(4|[39][39]*4)([06]|5[39]*4)*([28]|5[39]*[06]))|((5|[39][39]*5)|(4|[39][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06])))*((([28]|[39][39]*[28])|(4|[39][39]*4)([06]|5[39]*4)*(4|5[39]*[28]))|((5|[39][39]*5)|(4|[39][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*(([06]|[17][39]*[28])|([28]|[17][39]*4)([06]|5[39]*4)*(4|5[39]*[28]))))|((((5|[17][39]*[17])|([28]|[17][39]*4)([06]|5[39]*4)*([39]|5[39]*[17]))|(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((5|[17][39]*[17])|([28]|[17][39]*4)([06]|5[39]*4)*([39]|5[39]*[17])))|(((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06]))|(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06])))((([06]|[39][39]*[06])|(4|[39][39]*4)([06]|5[39]*4)*([28]|5[39]*[06]))|((5|[39][39]*5)|(4|[39][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06])))*((([17]|[39][39]*[17])|(4|[39][39]*4)([06]|5[39]*4)*([39]|5[39]*[17]))|((5|[39][39]*5)|(4|[39][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((5|[17][39]*[17])|([28]|[17][39]*4)([06]|5[39]*4)*([39]|5[39]*[17]))))(((([39]|5[39]*[17])|([06]|5[39]*4)([06]|5[39]*4)*([39]|5[39]*[17]))|(([17]|5[39]*5)|([06]|5[39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((5|[17][39]*[17])|([28]|[17][39]*4)([06]|5[39]*4)*([39]|5[39]*[17])))|((([28]|5[39]*[06])|([06]|5[39]*4)([06]|5[39]*4)*([28]|5[39]*[06]))|(([17]|5[39]*5)|([06]|5[39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06])))((([06]|[39][39]*[06])|(4|[39][39]*4)([06]|5[39]*4)*([28]|5[39]*[06]))|((5|[39][39]*5)|(4|[39][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06])))*((([17]|[39][39]*[17])|(4|[39][39]*4)([06]|5[39]*4)*([39]|5[39]*[17]))|((5|[39][39]*5)|(4|[39][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((5|[17][39]*[17])|([28]|[17][39]*4)([06]|5[39]*4)*([39]|5[39]*[17]))))*((((4|5[39]*[28])|([06]|5[39]*4)([06]|5[39]*4)*(4|5[39]*[28]))|(([17]|5[39]*5)|([06]|5[39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*(([06]|[17][39]*[28])|([28]|[17][39]*4)([06]|5[39]*4)*(4|5[39]*[28])))|((([28]|5[39]*[06])|([06]|5[39]*4)([06]|5[39]*4)*([28]|5[39]*[06]))|(([17]|5[39]*5)|([06]|5[39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06])))((([06]|[39][39]*[06])|(4|[39][39]*4)([06]|5[39]*4)*([28]|5[39]*[06]))|((5|[39][39]*5)|(4|[39][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*((4|[17][39]*[06])|([28]|[17][39]*4)([06]|5[39]*4)*([28]|5[39]*[06])))*((([28]|[39][39]*[28])|(4|[39][39]*4)([06]|5[39]*4)*(4|5[39]*[28]))|((5|[39][39]*5)|(4|[39][39]*4)([06]|5[39]*4)*([17]|5[39]*5))(([39]|[17][39]*5)|([28]|[17][39]*4)([06]|5[39]*4)*([17]|5[39]*5))*(([06]|[17][39]*[28])|([28]|[17][39]*4)([06]|5[39]*4)*(4|5[39]*[28])))))*$

参考资料

  1. 知乎:正则表达式如何匹配 3 的倍数?
  2. How to convert finite automata to regular expressions?
  3. Arden定理
赞(0) 打赏
未经允许不得转载:XINDOO » 用正则表达式匹配3的任意倍数

评论 1

  1. #1

    如果我说你牛,那是在侮辱你,你狠牛

    匿名4个月前 (12-02)回复

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫打赏

微信扫一扫打赏

登录

找回密码

注册