实现一个 DFA 正则表达式引擎 - 4. DFA 的最小化
(正则引擎已完成,Github)
最小化 DFA 是引擎中另外一个略繁琐的点(第一个是构建语法树)。
基本思路是,先对 DFA 进行重命名,然后引入一个拒绝态 0,定义所有状态经过非接受字符转到状态 0,0 接受所有字符转换为自身。也就是说我们先建立一个转换表,然后把第一行填写为:
state | a | b | c | d | e | f | g | h | ... |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
再之后,我们讲 DFA 的其余状态从 1 开始重命名,填入状态表。代码实现如下:
// rename all states for (Set<NFAState> nfaState : oriDFATransitionMap.keySet()) { if (initStateAfterRenaming == -1 && nfaState.equals(initClosure)) { initStateAfterRenaming = renamingStateID; // record init state id } stateRenamingMap.put(nfaState, renamingStateID++); } renamedDFATransitionTable.put(0, newRejectState()); // the reject state 0 finalFlags.put(0, false); // construct renamed dfa transition table for (Map.Entry<Set<NFAState>, Map<Character, Set<NFAState>>> entry : oriDFATransitionMap.entrySet()) { renamingStateID = stateRenamingMap.get(entry.getKey()); int[] state = newRejectState(); for (Map.Entry<Character, Set<NFAState>> row : entry.getValue().entrySet()) { state[row.getKey()] = stateRenamingMap.get(row.getValue()); } renamedDFATransitionTable.put(renamingStateID, state); if (entry.getKey().contains(finalNFAState)) { finalFlags.put(renamingStateID, true); } else finalFlags.put(renamingStateID, false); }
这里有一个 finalFlags 用来记录哪些 DFA 的终态包含了 NFA 的终态 1。(这些状态都作为 DFA 的终态)
接下来,把所有状态分为终态作为一个 group,非终态作为另一个 group:
// split states to final states and non-final states Map<Integer, Integer> groupFlags = new HashMap<>(); for (int i = 0; i < finalFlags.size(); i++) { boolean b = finalFlags.get(i); if (b) groupFlags.put(i, 0); else groupFlags.put(i, 1); }
我们定义任意 group 中的任意状态的转换规则为:接受某个字符转移到某个 group(区别于转换到某个状态)。
不停地遍历所有的 group,把同一个 group 中具有不同转换规则的状态分隔为不同的 group。
do { // splitting, group id is the final state id preGroupTotal = groupTotal; for (int sensitiveGroup = 0; sensitiveGroup < preGroupTotal; sensitiveGroup++) { // <target group table, state id set> Map<Map<Integer, Integer>, Set<Integer>> invertMap = new HashMap<>(); for (int sid = 0; sid < groupFlags.size(); sid++) { //use state id to iterate int group = groupFlags.get(sid); if (sensitiveGroup == group) { Map<Integer, Integer> targetGroupTable = new HashMap<>(CommonSets.ENCODING_LENGTH); for (char ch = 0; ch < CommonSets.ENCODING_LENGTH; ch++) { int targetState = renamedDFATransitionTable.get(sid)[ch]; int targetGroup = groupFlags.get(targetState); targetGroupTable.put((int) ch, targetGroup); } Set<Integer> stateIDSet = invertMap.get(targetGroupTable); if (stateIDSet == null) { stateIDSet = new HashSet<>(); invertMap.put(targetGroupTable, stateIDSet); } stateIDSet.add(sid); // gather all sids having the same target group table into this set } } boolean first = true; for (Set<Integer> stateIDSet : invertMap.values()) { if (first) { first = false; } else { for (int sid : stateIDSet) { groupFlags.put(sid, groupTotal); } groupTotal++; } } } } while (preGroupTotal != groupTotal);
如此分到不可再分,再把每一个 group 中的所有状态合并为同一个状态,这样我们就得到了 group 个状态,就完成了 DFA 的最小化。
接着是再次确定整个 DFA 的初态、终态和拒绝态。我们只要把包含这些状态的 group 作为最小化之后的相应状态就可以了。
// determine initial group state is = groupFlags.get(initStateAfterRenaming); // determine reject group state rs = groupFlags.get(0); // determine final group states Set<Integer> finalGroupFlags = new HashSet<>(); for (int i = 0, groupFlagsSize = groupFlags.size(); i < groupFlagsSize; i++) { Integer groupFlag = groupFlags.get(i); if (finalFlags.get(i)) { finalGroupFlags.add(groupFlag); } } fs = new boolean[groupTotal]; for (int i = 0; i < groupTotal; i++) { fs[i] = finalGroupFlags.contains(i); }
最后一步是把 DFA 转换为一个以数组形式存放的状态表,就可以用来进行快速而准确的正则匹配了。
// construct the final transition table transitionTable = new int[groupTotal][]; for (int groupID = 0; groupID < groupTotal; groupID++) { for (int sid = 0; sid < groupFlags.size(); sid++) { if (groupID == groupFlags.get(sid)) { int[] oriState = renamedDFATransitionTable.get(sid); int[] state = new int[CommonSets.ENCODING_LENGTH]; for (char ch = 0; ch < CommonSets.ENCODING_LENGTH; ch++) { int next = oriState[ch]; state[ch] = groupFlags.get(next); } transitionTable[groupID] = state; break; } } }
至此,我们的整个 DFA 正则表达式引擎就构建完成了。
之后我用一个 apache log 的正则做了一下性能测试,这个引擎的匹配速度比 JDK 自带的正则要快至少一倍,还是有一定的实用性的,所以我把它放到了 Github 上,欢迎下载源码。
个人水平有限,代码中一定有不少尚待优化的地方,如有建议请不吝赐教。