好的,没问题。
C++ 自定义 Regex 引擎:利用 NFA/DFA 进行模式匹配与性能优化
大家好,今天我们来聊聊如何用 C++ 构建一个自定义的正则表达式引擎。我们将深入探讨 NFA(非确定有限自动机)和 DFA(确定有限自动机)在模式匹配中的应用,并讨论一些性能优化的策略。
1. 正则表达式引擎概述
一个正则表达式引擎主要负责两件事:
- 解析正则表达式: 将输入的正则表达式字符串转换为内部表示,方便后续处理。
- 模式匹配: 使用内部表示在目标文本中查找匹配的子串。
常见的正则表达式引擎实现方式有两种:
- 回溯 (Backtracking): 这种方法基于正则表达式直接进行匹配,在遇到不匹配时会回溯到之前的状态,尝试其他的匹配路径。 优点是实现简单,支持的特性丰富。 缺点是性能可能较差,特别是对于复杂的正则表达式和较长的目标文本,容易出现“灾难性回溯” (catastrophic backtracking) 的问题。
- 有限自动机 (Finite Automata): 这种方法将正则表达式转换为有限自动机,然后利用自动机进行匹配。 优点是匹配速度快,时间复杂度是线性的。 缺点是实现相对复杂,可能需要消耗较多的内存。
今天我们主要讨论基于有限自动机的方式,并且会从 NFA 开始,最终优化到 DFA。
2. 正则表达式语法和抽象语法树 (AST)
首先,我们要定义一个简单的正则表达式语法,并实现一个能够将正则表达式字符串解析成抽象语法树 (AST) 的解析器。 让我们先定义我们支持的简单语法:
- 字符 (Character):
a,b,c, … - 连接 (Concatenation):
ab,abc, … - 或 (Alternation):
a|b,a|b|c, … - 重复 (Kleene Star):
a*,(ab)*, … - 括号 (Grouping):
(a),(a|b), …
下面是用 C++ 表示 AST 节点的代码:
#include <iostream>
#include <string>
#include <vector>
enum class NodeType {
CHARACTER,
CONCATENATION,
ALTERNATION,
KLEENE_STAR,
GROUP
};
class ASTNode {
public:
NodeType type;
char character;
std::vector<ASTNode*> children;
ASTNode(NodeType type) : type(type), character(0) {}
ASTNode(NodeType type, char character) : type(type), character(character) {}
~ASTNode() {
for (ASTNode* child : children) {
delete child;
}
}
};
// 辅助函数,用于打印 AST 结构
void printAST(ASTNode* node, int indent = 0) {
for (int i = 0; i < indent; ++i) {
std::cout << " ";
}
switch (node->type) {
case NodeType::CHARACTER:
std::cout << "CHARACTER: " << node->character << std::endl;
break;
case NodeType::CONCATENATION:
std::cout << "CONCATENATION" << std::endl;
break;
case NodeType::ALTERNATION:
std::cout << "ALTERNATION" << std::endl;
break;
case NodeType::KLEENE_STAR:
std::cout << "KLEENE_STAR" << std::endl;
break;
case NodeType::GROUP:
std::cout << "GROUP" << std::endl;
break;
}
for (ASTNode* child : node->children) {
printAST(child, indent + 1);
}
}
// 简单的解析器示例 (仅支持连接和字符)
ASTNode* parseRegex(const std::string& regex) {
ASTNode* root = new ASTNode(NodeType::CONCATENATION);
for (char c : regex) {
ASTNode* charNode = new ASTNode(NodeType::CHARACTER, c);
root->children.push_back(charNode);
}
return root;
}
int main() {
std::string regex = "abc";
ASTNode* ast = parseRegex(regex);
printAST(ast);
delete ast; // 释放内存
return 0;
}
这个例子只是一个非常简化的解析器,只支持连接和字符。 一个完整的正则表达式解析器需要处理所有语法规则,并且需要处理优先级和结合性。 这通常使用递归下降解析器或者其他的解析器生成器来实现。 由于解析器的实现比较复杂,我们这里只提供一个简单的示例,重点放在后续的 NFA/DFA 构建和匹配上。
3. NFA (非确定有限自动机)
NFA 是一种状态机,它由以下几个部分组成:
- 状态集合 (States)
- 输入字母表 (Alphabet)
- 转移函数 (Transition Function): δ(state, input) -> 状态集合
- 起始状态 (Start State)
- 接受状态集合 (Accepting States)
与 DFA 相比,NFA 的转移函数可以返回一个状态集合,这意味着在同一个状态下,对于同一个输入,NFA 可以转移到多个不同的状态。 此外,NFA 还允许 ε-转移 (epsilon transition),即不消耗任何输入就可以进行状态转移。
3.1 NFA 的表示
我们可以用 C++ 来表示 NFA:
#include <iostream>
#include <vector>
#include <set>
#include <map>
class NFAState {
public:
int id; // 状态ID
bool isAccepting; // 是否是接受状态
NFAState(int id) : id(id), isAccepting(false) {}
};
class NFA {
public:
std::vector<NFAState*> states;
std::map<std::pair<int, char>, std::set<int>> transitions; // 转移函数: (state_id, input_char) -> set of state_ids
int startStateId;
NFA(int startStateId) : startStateId(startStateId) {}
~NFA() {
for (NFAState* state : states) {
delete state;
}
}
// 添加状态
NFAState* addState() {
NFAState* newState = new NFAState(states.size());
states.push_back(newState);
return newState;
}
// 添加转移
void addTransition(int fromStateId, char input, int toStateId) {
transitions[{fromStateId, input}].insert(toStateId);
}
// 添加 epsilon 转移
void addEpsilonTransition(int fromStateId, int toStateId) {
addTransition(fromStateId, '', toStateId); // 使用 '' 表示 epsilon 转移
}
// 打印 NFA 结构
void printNFA() {
std::cout << "NFA:" << std::endl;
std::cout << " Start State: " << startStateId << std::endl;
std::cout << " States:" << std::endl;
for (const auto& state : states) {
std::cout << " State " << state->id << (state->isAccepting ? " (Accepting)" : "") << std::endl;
}
std::cout << " Transitions:" << std::endl;
for (const auto& transition : transitions) {
std::cout << " (" << transition.first.first << ", ";
if (transition.first.second == '') {
std::cout << "epsilon";
} else {
std::cout << transition.first.second;
}
std::cout << ") -> {";
for (int toStateId : transition.second) {
std::cout << toStateId << " ";
}
std::cout << "}" << std::endl;
}
}
};
3.2 从正则表达式构建 NFA (Thompson 构造法)
Thompson 构造法是一种将正则表达式转换为 NFA 的经典算法。 它为每种正则表达式语法规则定义了相应的 NFA 构造方法。
-
字符 (Character)
c:--> (state) --c--> (state) --> -
连接 (Concatenation)
AB: 将 A 的 NFA 和 B 的 NFA 连接起来,A 的接受状态通过 ε-转移连接到 B 的起始状态。 -
或 (Alternation)
A|B: 创建一个新的起始状态和一个新的接受状态,然后从新的起始状态分别通过 ε-转移连接到 A 和 B 的起始状态,A 和 B 的接受状态通过 ε-转移连接到新的接受状态。 -
*重复 (Kleene Star) `A`**: 创建一个新的起始状态和一个新的接受状态,然后从新的起始状态通过 ε-转移连接到 A 的起始状态和新的接受状态,A 的接受状态通过 ε-转移连接到 A 的起始状态和新的接受状态。
下面是用 C++ 实现 Thompson 构造法的代码:
#include <iostream>
#include <vector>
#include <set>
#include <map>
// 前面定义的 NFAState 和 NFA 类省略...
// Thompson 构造法
NFA* constructNFA(ASTNode* astNode) {
if (astNode->type == NodeType::CHARACTER) {
NFA* nfa = new NFA(0);
NFAState* startState = nfa->addState();
NFAState* acceptState = nfa->addState();
acceptState->isAccepting = true;
nfa->addTransition(startState->id, astNode->character, acceptState->id);
return nfa;
} else if (astNode->type == NodeType::CONCATENATION) {
if (astNode->children.empty()) {
// 空连接,创建一个接受状态
NFA* nfa = new NFA(0);
NFAState* startState = nfa->addState();
startState->isAccepting = true;
return nfa;
}
NFA* nfa = constructNFA(astNode->children[0]);
for (size_t i = 1; i < astNode->children.size(); ++i) {
NFA* nextNFA = constructNFA(astNode->children[i]);
// 连接两个 NFA
for (NFAState* state : nfa->states) {
if (state->isAccepting) {
nfa->addEpsilonTransition(state->id, nextNFA->startStateId + nfa->states.size());
state->isAccepting = false; // 设置为非接受状态
}
}
// 合并 nextNFA 的状态和转移到 nfa
int offset = nfa->states.size();
for (NFAState* state : nextNFA->states) {
NFAState* newState = nfa->addState();
newState->isAccepting = state->isAccepting;
}
for (const auto& transition : nextNFA->transitions) {
nfa->addTransition(transition.first.first + offset, transition.first.second, *transition.second.begin() + offset); // 假设每个转移只有一个目标状态
}
delete nextNFA;
}
return nfa;
} else {
// 其他情况 (ALTERNATION, KLEENE_STAR, GROUP) 未实现
std::cerr << "Unsupported AST Node Type." << std::endl;
return nullptr;
}
}
int main() {
std::string regex = "abc";
ASTNode* ast = parseRegex(regex);
NFA* nfa = constructNFA(ast);
if (nfa) {
nfa->printNFA();
delete nfa;
}
delete ast;
return 0;
}
这个例子只实现了字符和连接的 Thompson 构造。 其他操作符的构造过程类似,需要根据其对应的 NFA 结构进行实现。
3.3 NFA 的匹配
NFA 的匹配过程需要维护一个当前状态集合,表示 NFA 当前可能处于的所有状态。对于每个输入字符,我们需要计算从当前状态集合出发,经过该字符能够到达的所有状态,并将结果作为新的当前状态集合。 此外,我们还需要在每次移动后,计算当前状态集合的 ε-闭包 (epsilon closure),即从当前状态集合中的每个状态出发,经过 ε-转移能够到达的所有状态。
#include <iostream>
#include <vector>
#include <set>
#include <map>
// 前面定义的 NFAState 和 NFA 类省略...
// 计算 epsilon 闭包
std::set<int> epsilonClosure(const NFA& nfa, const std::set<int>& states) {
std::set<int> closure = states;
std::vector<int> stack(states.begin(), states.end());
while (!stack.empty()) {
int stateId = stack.back();
stack.pop_back();
for (const auto& transition : nfa.transitions) {
if (transition.first.first == stateId && transition.first.second == '') { // epsilon 转移
for (int toStateId : transition.second) {
if (closure.find(toStateId) == closure.end()) {
closure.insert(toStateId);
stack.push_back(toStateId);
}
}
}
}
}
return closure;
}
// NFA 匹配
bool matchNFA(const NFA& nfa, const std::string& text) {
std::set<int> currentStates = {nfa.startStateId};
currentStates = epsilonClosure(nfa, currentStates);
for (char c : text) {
std::set<int> nextStates;
for (int stateId : currentStates) {
for (const auto& transition : nfa.transitions) {
if (transition.first.first == stateId && transition.first.second == c) {
for (int toStateId : transition.second) {
nextStates.insert(toStateId);
}
}
}
}
currentStates = epsilonClosure(nfa, nextStates);
}
// 检查是否存在接受状态
for (int stateId : currentStates) {
for (const auto& state : nfa.states) {
if (state->id == stateId && state->isAccepting) {
return true;
}
}
}
return false;
}
int main() {
std::string regex = "abc";
ASTNode* ast = parseRegex(regex);
NFA* nfa = constructNFA(ast);
if (nfa) {
nfa->printNFA();
std::string text = "abcdefg";
bool matched = matchNFA(*nfa, text);
if (matched) {
std::cout << "Text '" << text << "' matches the regex '" << regex << "'" << std::endl;
} else {
std::cout << "Text '" << text << "' does not match the regex '" << regex << "'" << std::endl;
}
delete nfa;
}
delete ast;
return 0;
}
4. DFA (确定有限自动机)
DFA 是一种特殊的 NFA,它的转移函数对于每个状态和每个输入字符都返回一个唯一的状态。 DFA 没有 ε-转移。 这意味着 DFA 在匹配过程中,当前状态始终是唯一的,从而避免了 NFA 中需要维护状态集合和计算 ε-闭包的开销。
4.1 从 NFA 转换到 DFA (子集构造法)
子集构造法是一种将 NFA 转换为 DFA 的经典算法。 它的基本思想是:DFA 的每个状态对应于 NFA 的一个状态集合。
算法步骤如下:
- 计算 NFA 的起始状态的 ε-闭包,作为 DFA 的起始状态。
- 对于 DFA 的每个未标记状态,计算从该状态出发,经过每个输入字符能够到达的 NFA 状态集合,并计算这些状态集合的 ε-闭包。 如果计算出的 ε-闭包已经存在于 DFA 中,则创建一个新的 DFA 状态,并将转移添加到 DFA 中。
- 重复步骤 2,直到 DFA 中没有未标记的状态。
- DFA 的接受状态是那些包含 NFA 接受状态的 DFA 状态。
#include <iostream>
#include <vector>
#include <set>
#include <map>
// 前面定义的 NFAState 和 NFA 类省略...
class DFAState {
public:
int id;
std::set<int> nfaStateIds; // 对应的 NFA 状态集合
bool isAccepting;
DFAState(int id, const std::set<int>& nfaStateIds) : id(id), nfaStateIds(nfaStateIds), isAccepting(false) {}
};
class DFA {
public:
std::vector<DFAState*> states;
std::map<std::pair<int, char>, int> transitions; // 转移函数: (state_id, input_char) -> state_id
int startStateId;
DFA(int startStateId) : startStateId(startStateId) {}
~DFA() {
for (DFAState* state : states) {
delete state;
}
}
// 添加状态
DFAState* addState(const std::set<int>& nfaStateIds) {
DFAState* newState = new DFAState(states.size(), nfaStateIds);
states.push_back(newState);
return newState;
}
// 添加转移
void addTransition(int fromStateId, char input, int toStateId) {
transitions[{fromStateId, input}] = toStateId;
}
// 打印 DFA 结构
void printDFA() {
std::cout << "DFA:" << std::endl;
std::cout << " Start State: " << startStateId << std::endl;
std::cout << " States:" << std::endl;
for (const auto& state : states) {
std::cout << " State " << state->id << " (NFA States: ";
for (int nfaStateId : state->nfaStateIds) {
std::cout << nfaStateId << " ";
}
std::cout << ")" << (state->isAccepting ? " (Accepting)" : "") << std::endl;
}
std::cout << " Transitions:" << std::endl;
for (const auto& transition : transitions) {
std::cout << " (" << transition.first.first << ", " << transition.first.second << ") -> " << transition.second << std::endl;
}
}
};
// 子集构造法
DFA* constructDFA(const NFA& nfa) {
std::map<std::set<int>, int> stateMap; // 用于记录 DFA 状态和对应的 ID
DFA* dfa = new DFA(0);
std::set<int> startStateIds = epsilonClosure(nfa, {nfa.startStateId});
DFAState* startState = dfa->addState(startStateIds);
stateMap[startStateIds] = startState->id;
std::vector<int> unmarkedStates = {startState->id};
while (!unmarkedStates.empty()) {
int dfaStateId = unmarkedStates.back();
unmarkedStates.pop_back();
DFAState* dfaState = dfa->states[dfaStateId];
// 找到所有输入字符
std::set<char> alphabet;
for (const auto& transition : nfa.transitions) {
if (transition.first.second != '') { // 排除 epsilon 转移
alphabet.insert(transition.first.second);
}
}
for (char input : alphabet) {
std::set<int> nextNFAStateIds;
for (int nfaStateId : dfaState->nfaStateIds) {
for (const auto& transition : nfa.transitions) {
if (transition.first.first == nfaStateId && transition.first.second == input) {
for (int toStateId : transition.second) {
nextNFAStateIds.insert(toStateId);
}
}
}
}
nextNFAStateIds = epsilonClosure(nfa, nextNFAStateIds);
if (nextNFAStateIds.empty()) {
continue; // 没有转移
}
int nextDFAStateId;
if (stateMap.find(nextNFAStateIds) == stateMap.end()) {
// 创建新的 DFA 状态
DFAState* nextDFAState = dfa->addState(nextNFAStateIds);
stateMap[nextNFAStateIds] = nextDFAState->id;
nextDFAStateId = nextDFAState->id;
unmarkedStates.push_back(nextDFAStateId);
} else {
nextDFAStateId = stateMap[nextNFAStateIds];
}
dfa->addTransition(dfaStateId, input, nextDFAStateId);
}
}
// 标记接受状态
for (DFAState* state : dfa->states) {
for (int nfaStateId : state->nfaStateIds) {
for (const auto& nfaState : nfa.states) {
if (nfaState->id == nfaStateId && nfaState->isAccepting) {
state->isAccepting = true;
break;
}
}
if (state->isAccepting) break;
}
}
return dfa;
}
int main() {
std::string regex = "abc";
ASTNode* ast = parseRegex(regex);
NFA* nfa = constructNFA(ast);
if (nfa) {
DFA* dfa = constructDFA(*nfa);
if (dfa) {
dfa->printDFA();
delete dfa;
}
delete nfa;
}
delete ast;
return 0;
}
4.2 DFA 的匹配
DFA 的匹配过程非常简单,从起始状态开始,对于每个输入字符,根据转移函数转移到下一个状态。 如果最终到达的状态是接受状态,则匹配成功。
#include <iostream>
#include <vector>
#include <set>
#include <map>
// 前面定义的 DFAState 和 DFA 类省略...
// DFA 匹配
bool matchDFA(const DFA& dfa, const std::string& text) {
int currentStateId = dfa.startStateId;
for (char c : text) {
auto transition = dfa.transitions.find({currentStateId, c});
if (transition == dfa.transitions.end()) {
return false; // 没有转移
}
currentStateId = transition->second;
}
// 检查是否是接受状态
for (const auto& state : dfa.states) {
if (state->id == currentStateId && state->isAccepting) {
return true;
}
}
return false;
}
int main() {
std::string regex = "abc";
ASTNode* ast = parseRegex(regex);
NFA* nfa = constructNFA(ast);
if (nfa) {
DFA* dfa = constructDFA(*nfa);
if (dfa) {
dfa->printDFA();
std::string text = "abcdefg";
bool matched = matchDFA(*dfa, text);
if (matched) {
std::cout << "Text '" << text << "' matches the regex '" << regex << "'" << std::endl;
} else {
std::cout << "Text '" << text << "' does not match the regex '" << regex << "'" << std::endl;
}
delete dfa;
}
delete nfa;
}
delete ast;
return 0;
}
5. 性能优化
- DFA 最小化: 使用 Hopcroft 算法或者其他 DFA 最小化算法,可以减少 DFA 的状态数量,从而减少内存消耗和提高匹配速度。
- 延迟 DFA 构建: 在实际应用中,可以将 DFA 的构建过程延迟到第一次匹配时进行。 如果正则表达式很少使用,则可以避免构建 DFA 的开销。
- 缓存 DFA: 将构建好的 DFA 缓存起来,以便下次使用。 可以使用 LRU (Least Recently Used) 缓存策略来管理 DFA 缓存。
- 特殊情况优化: 对于一些简单的正则表达式,可以直接使用字符串查找算法 (如 Knuth-Morris-Pratt 算法或 Boyer-Moore 算法) 进行匹配,而无需构建 NFA/DFA。
- SIMD 指令: 可以使用 SIMD (Single Instruction Multiple Data) 指令来并行处理多个字符,从而提高匹配速度。
- 代码生成: 可以将正则表达式编译成机器码,从而获得更高的执行效率。
6. 总结
我们学习了如何用 C++ 构建一个自定义的正则表达式引擎,从正则表达式语法解析到AST,再到NFA的Thompson构造法,以及NFA转换到DFA的子集构造法,最后DFA进行匹配。并讨论了一些性能优化策略。虽然只是一个简单的示例,但它涵盖了正则表达式引擎的核心概念。希望通过今天的讨论,大家能够对正则表达式引擎的实现有更深入的理解。
更多IT精英技术系列讲座,到智猿学院