正则表达式->NFA->DFA(C++实现)

本文介绍了一种从正则表达式构建确定有限自动机(DFA)的方法,包括正则表达式转换为后缀表达式、后缀表达式构造非确定有限自动机(NFA)及NFA转换为DFA的过程。

这学期在学编译原理,上周刚学完词法分析,作业是手动构造DFA并完成词法分析。然而优秀的人当然要以高标准要求自己,于是花了两天实现了输入正则表达式构造NFA和NFA转DFA的算法。

算法包括以下几个步骤:

  • 正则表达式->后缀表达式
  • 用后缀表达式构造NFA
  • 用NFA构造DFA
  • 最小化DFA(暂未完成)

正则表达式->后缀表达式

正则表达式的定义

算法中实现的正则表达式仅包含 ( ) ∣ ∗ ()|* ()运算符。为了方便实现,显式地增加了连接运算符^, 如abc*d会被修改为a ^ b ^ c * ^d。$字符在式子中的含义是空串。
预处理部分的代码:

char Lex::statusCode(char a)//非运算符返回本身,其他返回'a'
{//inputSet是不包含运算符的字符集
    if (inputSet.find(a) != inputSet.end())
        return 'a';
    return a;
}

void Lex::preprocess()
{//charSet是字符集,包含运算符
    assert(reg.size());
    int l = 0;
    for (const auto &item : reg)
    {
        assert(charSet.find(item) != charSet.end());//判断是否所有字符都合法
        if (item == '(') ++l;
        else if (item == ')') --l;
        assert(l >= 0);//判断括号是否匹配
    }
    assert(l == 0);//判断括号是否匹配
    //对于每两个相邻的字符,statusCode分别为a( aa )a *a *( )(时
    //需要在中间加上^
    for (int i = 0; i < reg.size() - 1; ++i)
    {
        int a = statusCode(reg[i]), b = statusCode(reg[i + 1]);
        if (a == 'a' && b == '(' || a == 'a' && b == 'a' || a == ')' && b == 'a' || a == '*' && b == 'a' ||
            a == '*' && b == '(' || a == ')' && b == '(')
            reg.insert(i + 1, "^");
    }
    std::cout << reg << '\n';
    reg = toSuffix(reg);//转换为后缀表达式
    std::cout << reg << '\n';
}

运算符优先级

从大到小: * ^ | (,右括号特殊处理(转后缀表达式时右括号不入栈,不需要优先级)
确定优先级后即可将中缀形式的正则表达式转换为后缀形式。
(a | b) * ^ a ^ b ^ b 将被转换为
a b | * a ^ b ^ b ^

bool isOperator(char c)
{//判断是不是运算符
    switch (c)
    {
        case '*':
        case '|':
        case '^':
            return true;
        default:
            return false;
    }
}


int getPriority(int c)
{//运算符的优先级
    int level = 0; // 优先级
    switch (c)
    {
        case '(':
            level = 1;
            break;
        case '|':
            level = 2;
            break;
        case '^':
            level = 3;
            break;
        case '*':
            level = 4;
            break;
        default:
            break;
    }
    return level;
}

string toSuffix(const string &expr)
{
    stack<char> op;
    string suffix;
    for (const auto &c: expr)
    {
        if (isOperator(c))
        {//是运算符
            if (op.empty())//栈空,直接入栈
                op.emplace(c);
            else
            {//优先级更大的运算符全部出栈
                while (!op.empty())
                {
                    int t = op.top();
                    if (getPriority(c) <= getPriority(t))
                    {
                        op.pop();
                        suffix.push_back(t);
                    }
                    else
                        break;
                }
                op.emplace(c);
            }
        }
        else
        {
            if (c == '(')//左括号直接入栈
                op.emplace(c);
            else if (c == ')')
            {//遇到右括号,一直出栈,直到遇到左括号
                while (op.top() != '(')
                {
                    suffix.push_back(op.top());
                    op.pop();
                }
                op.pop();
            }
            else
            	suffix.push_back(c);//操作数直接放入表达式中
        }
    }
    while (!op.empty())
    {//取出剩余的运算符
        suffix.push_back(op.top());
        op.pop();
    }
    return suffix;
}

后缀表达式构造NFA

定义NFA类

数据成员: 开始状态、结束状态和图

  • 参考龙书上的算法,这里构造出的NFA只会有一个开始状态和一个结束状态
  • start和end其实没啥用(但还是写在这里了),因为该算法构造出的NFA的开始状态一定是0,结束状态一定是最后一个状态
  • 图中包含了NFA五元组中的状态集合转换函数
  • 图用vector<unordered_map<char, vector>>存储
  • 字符集存储在之前提到的inputSet中
struct NFA
    {
        vector<unordered_map<char, vector<int>>> G;
        int start, end;

        NFA()
        {
            G.resize(1);
            start = end = 0;
        }

        size_t size() const
        {
            return G.size();
        }

        unordered_map<char, vector<int>> &operator[](int n)
        {
            assert(n < G.size());
            return G[n];
        }

        NFA &operator+=(NFA b)
        {//合并两个图
            int offset = size();
            for (int i = 0; i < b.size(); ++i)
            {
                G.push_back(b[i]);
                for (auto &k: G.back())
                    for (auto &v: k.second)
                        v += offset;
            }
            return *this;
        }
    };

需要注意的是operator+=。因为算法中需要连接两个图,所以重载operato+=来完成合并。因为两个图的节点编号都是从0开始的,所以b连接到a后面时,b中所有边指向的节点编号都要加上一个offset(a的size)

构造NFA

(以下图片截自龙书第二版)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
按照上面的五张图的方式建图,即可完成NFA的构造
//代码中$是空串

void Lex::buildNFA()
{
    stack<NFA> stk;
    for (auto &c: reg)
    {
        if (!isOperator(c))//前两张图片
        {
            NFA a;
            a.G.resize(2);
            a.end = 1;
            a[0][c] = {a.end};
            stk.emplace(a);//每次新产生的子NFA都放入栈中
        }
        else
        {//后三张图片
            switch (c)
            {
                case '|':
                {
                    NFA tmp;
                    NFA b = stk.top();
                    stk.pop();
                    NFA a = stk.top();
                    stk.pop();
                    size_t size1 = tmp.size();
                    tmp += a;
                    tmp[tmp.start]['$'].emplace_back(a.start + size1);

                    size_t size2 = tmp.size();
                    tmp += b;
                    tmp[tmp.start]['$'].emplace_back(b.start + size2);
                    tmp[a.end + size1]['$'] = {int(tmp.size())};
                    tmp[b.end + size2]['$'] = {int(tmp.size())};
                    tmp.end = tmp.size();
                    tmp.G.emplace_back(unordered_map<char, vector<int>>());
                    stk.emplace(tmp);//每次新产生的子NFA都放入栈中
                    break;
                }
                case '*':
                {
                    NFA tmp;
                    NFA a = stk.top();
                    stk.pop();
                    size_t size1 = tmp.size();
                    tmp += a;
                    tmp[tmp.start]['$'].emplace_back(a.start + size1);
                    tmp[a.end + size1]['$'].emplace_back(a.start + size1);
                    size_t s = tmp.size();
                    tmp[a.end + size1]['$'].emplace_back(s);
                    tmp.end = s;
                    tmp.G.emplace_back(unordered_map<char, vector<int>>());
                    tmp[tmp.start]['$'].emplace_back(s);
                    stk.emplace(tmp);//每次新产生的子NFA都放入栈中
                    break;
                }
                case '^':
                {
                    NFA b = stk.top();
                    stk.pop();
                    NFA a = stk.top();
                    stk.pop();
                    a.G.pop_back();
                    size_t s = a.size();
                    a += b;
                    a.end = b.end + s;
                    stk.emplace(a);//每次新产生的子NFA都放入栈中
                    break;
                }
                default:
                    assert(0);
            }
        }
    }
    nfa = stk.top();
}

用NFA构造DFA

DFA类的定义

struct DFA
{
    map<int, map<char, int>> G;//图
    unordered_set<int> end;//结束状态集合
    bool match(const string &s,char (*type)(char c))
    {
        int now = 0;
        for (auto &i: s)
        {
            char c = type(i);
            if (G[now].count(c))
                now = G[now][c];
            else
                return false;
        }
        return end.count(now);
    }
};
struct DStat
{
    set<int> stats;
    int id;

    bool operator<(const DStat &d) const
    {
        return stats < d.stats;
    }

    bool operator==(const DStat &d) const
    {
        return stats == d.stats;
    }
};

子集构造法

定义:
m o v e ( T , a ) move(T, a) move(T,a): NFA中,一个状态集合T中通过一个字符a可以转移到的状态集合为
ϵ − c l o s u r e ( s ) \epsilon-closure(s) ϵclosure(s): 由状态s通过任意数量空边能转移到的状态集合
ϵ − c l o s u r e ( T ) \epsilon-closure(T) ϵclosure(T): 一个状态集合T中的所有状态通过任意数量的空边能转移到的状态集合为
对于NFA中的一个状态集合T,它经过一个字符a能转移到的状态集合即为
ϵ − c l o s u r e ( m o v e ( T , a ) ) \epsilon-closure(move(T, a)) ϵclosure(move(T,a))
而初始状态下,NFA可以位于 ϵ − c l o s u r e ( s t a r t ) \epsilon-closure(start) ϵclosure(start)的所有状态
下面是龙书中的伪代码和对应的我的实现:
在这里插入图片描述

void Lex::buildDFA()
{
    vector<DStat> Dstats;
    Dstats.emplace_back(e_closure(nfa.start));
    set<DStat> vis;
    vis.insert(Dstats[0]);
    for (int i = 0; i < Dstats.size(); ++i)
    {
        for (auto c: inputSet)
        {
            if (c == '$')
                continue;
            auto U = e_closure(move(Dstats[i], c));
            if (U.stats.empty())
                continue;
            if (vis.find(U) == vis.end())
            {
                Dstats.emplace_back(U);
                if (U.stats.find(nfa.end) != U.stats.end())
                    dfa.end.insert(std::find(Dstats.begin(), Dstats.end(), U) - Dstats.begin());//U在DStat中的下标
                vis.insert(U);
            }
            dfa.G[i][c] = std::find(Dstats.begin(), Dstats.end(), U) - Dstats.begin();//U在DStat中的下标
        }
    }
    for (int i = 0; i < Dstats.size(); ++i)
    {
        std::cout << i << ": {";
        for (auto &j:Dstats[i].stats)
            std::cout << j << ' ';
        std::cout << "}\n";
    }
    for (auto &i: dfa.G)
        for (auto &j: i.second)
        {
            std::cout << i.first << ' ' << j.first << ' ' << j.second << '\n';
        }
}

在这里插入图片描述

Lex::DStat Lex::e_closure(int s)
{
    DStat stat;
    stat.stats.insert(s);
    stack<int> stk;
    stk.emplace(s);
    stat.stats.insert(s);
    while (!stk.empty())
    {
        int i = stk.top();
        stk.pop();
        for (auto &v: nfa[i]['$'])
        {
            if (stat.stats.find(v) != stat.stats.end()) continue;
            stk.emplace(v);
            stat.stats.insert(v);
        }
    }
    return stat;
}

Lex::DStat Lex::e_closure(Lex::DStat T)
{//这里实现和伪代码不太一样,我直接对多个e_closure(s)取并了
    DStat stat;
    for (auto &i: T.stats)
    {
        auto tmp = e_closure(i);
        for (auto &v: tmp.stats)
            stat.stats.insert(v);
    }
    return stat;
}

Lex::DStat Lex::move(const Lex::DStat &T, char a)
{
    DStat stat;
    for (auto &i: T.stats)
        if (nfa[i].find(a) != nfa[i].end())
            for (auto &v: nfa[i][a])
            {
                if (stat.stats.find(v) == stat.stats.end())
                {
                    stat.stats.insert(v);
                }
            }
    return stat;
}

至此就完成了DFA的构造

为了检验代码的正确性,我用如下正则表达式构造了识别数字的DFA,并通过了leetcode65 有效数字

  • ( ∣ + ∣ − ) a a ∗ ( |+|-)aa*( +)aa(|.a*)( ∣ e ( |e( e(|+|-)aa*)|( ∣ + ∣ − ) a ∗ ( a ∣ . a a ∗ ) ( |+|-)a*(a|.aa*)( +)a(a.aa)(|e($|+|-)aa*)
    在这里插入图片描述
    从内存消耗没有击败100%这一点也能看出算法还有进一步优化的空间(DFA的最小化)

博客中仅为部分代码。详细代码见github中的Lex和utils

最小化DFA

(下次一定写

<think>好的,我现在需要帮助用户获取用C++实现正则表达式DFA最小化及词法分析程序的完整代码,包括头文件和源文件的分类。首先,我需要回顾用户提供的引用内容,看看有没有相关的实现线索。 引用[1]提到需要创建一个词法分析程序,使用DFANFA来分析正规文法,输出符号表。引用[2]讨论了使用DFA进行模式匹配的算法,引用[3]则涉及将后续遍历序列转换为NFA的过程。这些引用提供了项目的基本要求和部分算法,但并没有给出具体的代码实现。 用户的需求是完整的C++代码,包括头文件和源文件的分类。因此,我需要考虑如何结构化这个项目。通常,这样的项目会分为几个模块:正则表达式NFANFADFADFA最小化,以及词法分析器。每个模块可能需要单独的类来处理,比如RegexParser、NFAState、DFAState等。 首先,正则表达式NFA的转换通常使用Thompson算法。需要处理的操作包括连接、选择和闭包。每个NFA状态可能有多个转移,因此需要合适的数据结构来管理。接下来,NFADFA的转换使用子集构造法,这涉及到计算ε闭包和状态集的转换。然后,DFA最小化通常使用Hopcroft算法来合并等价状态,减少状态数量。 词法分析器部分需要读取输入字符串,并使用构建好的DFA进行状态转移,最终确定符号类型。符号类型包括关键词、标识符、常量、界符和操作符,这可能需要一个符号表来管理。 在代码组织上,头文件可能包括类声明和数据结构定义,而源文件实现具体的方法。例如,NFA.h和NFA.cpp处理NFA的构建,DFA.h和DFA.cpp处理DFA的转换和最小化,Lexer.h和Lexer.cpp负责词法分析。 需要注意的是,用户可能需要处理输入的正则表达式,将其转换为后缀形式(如逆波兰表示法),然后再构建NFA。这部分在引用[3]中提到过post2nfa函数,可能需要实现re2post函数来处理正则表达式的转换。 此外,代码需要处理各种正则表达式操作符,如*、|、()等,并正确转义特殊字符。在DFA最小化过程中,需要比较状态组的等价性,并分割不可区分的组。 用户可能需要测试代码的正确性,因此提供示例输入和输出的说明会有所帮助。例如,如何处理正则表达式(a|b)*abb”并生成对应的DFA,然后最小化它,最后用这个词法分析器分析输入的字符串。 最后,需要确保代码的模块化和可读性,每个部分职责明确,便于维护和扩展。可能还需要异常处理,比如无效的正则表达式或输入字符串的处理。</think>根据您的要求,我将为您提供一个结构化实现的框架和关键代码示例。完整工程需要约2000行代码,这里提供核心算法和类设计的实现思路。 ### 一、工程结构 ``` regex_lexer/ ├── include/ │ ├── NFA.h // NFA状态和转换定义 │ ├── DFA.h // DFA最小化算法 │ ├── RegexParser.h // 正则表达式解析 │ └── Lexer.h // 词法分析接口 ├── src/ │ ├── NFA.cpp │ ├── DFA.cpp │ ├── RegexParser.cpp │ └── Lexer.cpp └── main.cpp // 测试用例 ``` ### 二、核心类定义(NFA.h节选) ```cpp struct NFAState { int id; bool is_end; std::unordered_map<char, std::set<NFAState*>> transitions; }; class NFA { public: NFAState* start; NFAState* end; static NFA create_basic(char c); static NFA concat(NFA& first, NFA& second); static NFA union_op(NFA& a, NFA& b); static NFA kleene_star(NFA& nfa); }; ``` ### 三、正则表达式解析(RegexParser.cpp关键代码) ```cpp string RegexParser::re2post(string regex) { stack<char> op_stack; string output; // 处理运算符优先级和转义字符 for (char c : regex) { if (is_operator(c)) { while (!op_stack.empty() && precedence(op_stack.top()) >= precedence(c)) { output += op_stack.top(); op_stack.pop(); } op_stack.push(c); } else if (c == '(') { op_stack.push(c); } else if (c == ')') { // ...处理括号匹配 } else { output += c; } } // ...弹出剩余操作符 return output; } ``` ### 四、NFADFA转换(DFA.cpp节选) ```cpp DFAState* DFAConverter::subset_construction(NFAState* start) { auto closure = epsilon_closure({start}); DFAState* dfa_start = new DFAState(closure); unordered_set<DFAState*> unmarked; unmarked.insert(dfa_start); while (!unmarked.empty()) { DFAState* cur = *unmarked.begin(); unmarked.erase(unmarked.begin()); for (char c : get_alphabet()) { auto moved = move(cur->nfa_states, c); auto new_closure = epsilon_closure(moved); if (!new_closure.empty()) { // 创建或查找DFA状态 // 建立状态转移关系 } } } return dfa_start; } ``` ### 五、DFA最小化实现(Hopcroft算法) ```cpp void DFAMinimizer::minimize() { // 初始划分为接受状态和非接受状态 vector<set<int>> partitions = {accept_states, non_accept_states}; while (true) { vector<set<int>> new_partitions; for (auto& P : partitions) { // 寻找可分割的等价类 map<pair<char, int>, set<int>> split_map; for (int state : P) { // 计算每个状态在输入符号下的转移目标分区 vector<int> key; for (char c : alphabet) { int target = transition_table[state][c]; key.push_back(get_partition_index(target, partitions)); } split_map[make_pair(c, key)].insert(state); } // 将分割结果加入新分区 for (auto& entry : split_map) { new_partitions.push_back(entry.second); } } if (new_partitions.size() == partitions.size()) break; partitions = new_partitions; } // 合并等价状态 } ``` ### 六、词法分析器实现(Lexer.cpp) ```cpp vector<Token> Lexer::analyze(string input) { vector<Token> tokens; int pos = 0; while (pos < input.length()) { DFAState* current = dfa_start; int last_accept = -1; int end_pos = pos; for (int i = pos; i < input.length(); i++) { char c = input[i]; auto it = current->transitions.find(c); if (it == current->transitions.end()) break; current = it->second; if (current->is_accept) { last_accept = i; end_pos = i + 1; } } if (last_accept != -1) { string lexeme = input.substr(pos, end_pos - pos); tokens.push_back({determine_token_type(lexeme), lexeme}); pos = end_pos; } else { // 处理错误 pos++; } } return tokens; } ``` 完整实现需要补充以下内容: 1. 字符集处理(支持转义字符) 2. 语法分析树构建(Thompson算法) 3. 状态转移表优化 4. 符号类型判断逻辑 测试用例示例: ```cpp int main() { RegexParser parser; NFA nfa = parser.build_nfa("(a|b)*abb"); DFA dfa = DFAConverter::convert(nfa); DFAMinimizer minimizer(dfa); minimizer.minimize(); Lexer lexer(dfa); auto tokens = lexer.analyze("abbaabb"); // 输出符号表 } ``` 相关问题
评论 4
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值