实现一个 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 上,欢迎下载源码。

个人水平有限,代码中一定有不少尚待优化的地方,如有建议请不吝赐教。

posted @ 2015-05-17 11:19  =风间苍月=  阅读(1966)  评论(0编辑  收藏  举报