正则表达式转NFA,DFA,最小化DFA
Exp 2:正则表达式转NFA,DFA,最小化DFA
(1)正则表达式应该支持单个字符,运算符号有: 连接 选择(|) 闭包(*) 正闭包(+) 可选(?) 括号
(2)要提供一个源程序编辑界面,让用户输入表示生成流水线处理过程的正则表达式(可保存、打开正则表达式文件)
(3)需要提供窗口以便用户可以查看转换得到的NFA(用状态转换表呈现即可)
(4)需要提供窗口以便用户可以查看转换得到的DFA(用状态转换表呈现即可)
(5)需要提供窗口以便用户可以查看转换得到的最小化DFA(用状态转换表呈现即可)
结构的设计
1. 基本节点
有状态,字符(用来记录‘^’,'# ',其他字符这些),指向下一个节点的指针。用来表示一个圆圈以及一个箭头。
struct Linknode
{int stateNum = -1;QChar worker = '#';Linknode* next = nullptr;Linknode(int s, QChar c, Linknode* n=nullptr){stateNum = s;worker = c;next = n;}
};
2.NFA的基本节点,用来传递参数。(函数需要接受初态和终态)
struct NFA{Linknode* start = nullptr;Linknode* end = nullptr;NFA(Linknode* s, Linknode* e){start = s;end = e;}
};
3.DFA基本节点
因为DFA一个节点要存放一个字符集合,所以使用 QSet结构
struct dfaNode{QSet<int> statu_set;QChar worker = '#';dfaNode* next = nullptr;
};
4.minDFA
struct minDfaNode{int state_num;QChar worker = '#';bool finish = false;minDfaNode* next = nullptr;
};
5.窗口的统一结构
class Linklist{public:Linklist();QVector<Linknode*> vertexs; //NFA邻接表的表头QVector<dfaNode*> dfalist; //DFA邻接表的表头QVector<minDfaNode*> mindfalist; //最小化邻接表的表头Linknode* first; dfaNode* dfa_first; minDfaNode* mindfa_first;
}
6.实际在QT窗口使用的结构是;
class MainWindow : public QMainWindow{Q_OBJECT
public:QString raw_r; //输入的正则表达式QString process_r; //后缀表达式Linklist* link_table = new Linklist(); //总的头int state_num = 0;QString xlex;QString save_path;QStack<NFA*> nfa_stack;QVector<QChar> alphabet;QString Process(QString raw);
}
1.输入正则表达式
第一步:特殊运算符: ‘&’ , ‘|’ , ‘*’ , ‘+’ , ‘?’ , ‘[ ]’
扫描一行符号
1.去掉空格
2.方括号要转换形式,从[1-5],变成 (1|2|3|4|5)这样方便进一步转换
因为连接符号在正则表达式中是隐形的,所以为了转换,需要手动加上‘&’表示连接符号。
- 两个字符之间需要加一个 “&”
这几种情况要加& : “a a” 、“a (” 、“* a” 、“* (” 、“) a” 、 “) (”
比如原字符串:a|bcs[1-3]asds
加&后 : a|b&c&s&(1|2|3)&a&s&d&s
开始遍历
如果栈内的运算符优先级大于等于当前运算符,则弹出栈内运算符
//添加连接符'&'int length = raw.length();QString rawadd;for(int i=0;i<length;i++){rawadd.append(raw[i]);if(i < length-1 && ((isAlpha(raw[i]) && isAlpha(raw[i+1])) || (isAlpha(raw[i]) && raw[i+1] == '(') || (raw[i] == '*' && isAlpha(raw[i+1]))|| (raw[i] == '*' && raw[i+1] == '(') || (raw[i] == ')' && isAlpha(raw[i+1])) || (raw[i] == ')' && raw[i+1] == '('))){rawadd.append('&');}}
字符可以直接加入到 总的字符串中,因为在中缀和后缀表达式中,数字的位置都是一样的。遇到5大符号( ‘&’ , ‘|’ , ‘*’ , ‘+’ , ‘?’),则需要单独处理,进入栈中,这块还有一些逻辑。
‘[ ]’
方括号太抽象, 这里转换成更具体的范围 (||||) 用圆括号和 || 来表示
QString::toLatin1是相当于 ASCii码不包含中文的遇到中文默认转换为ascii码。
bracket()函数 , 即当遇到[ - ]这段的时候,用 (||||)替代,其他原封不动的保存。
QString bracket(QString raw)
{QString p;int i=0;while(i<raw.length()){if(raw[i] == '['){p.append('(');char c1 = raw[++i].toLatin1();p.append(c1);i++;char c3 = raw[++i].toLatin1();for(int j=(c1-'0')+1;j<=(c3-'0');j++){p.append('|');p.append(char(j+'0'));}i++;p.append(')');}else{p.append(raw[i]);}i++;}return p;
}
正则表达式是中缀的,但是为了方便使用后缀表达式 (其实也可以不转换,直接用双栈结构来完成,一个符号栈,一个字母栈。但是没有这么做。)
中缀表达式 9+(3-1)x3+10÷2
后缀表达式9 3 1 - 3 x + 10 2 ÷ +
开两个栈结构,一个放数字,一个放符号。
从左到右。9 入数字栈
+号 入符号栈(目前栈空,栈空就进栈)
( 入符号栈
目前栈里从上到下:( +
3入数字栈
当前表达式为 9 3
- 入栈
目前栈里从上到下:-( +
正则表达式中总共有以下的符号。
算法思路如下:
QString process, 和一个栈结构 operator
process相当于数字栈, operator 相当于符号栈
//转换成后缀表达式int i=0;length = rawadd.length();QString process;while(i < length){if(isAlpha(rawadd[i]))//遇到字符直接输出{process.append(rawadd[i]);}else if(rawadd[i] == '('){operate.push(rawadd[i]);}else if(rawadd[i] == ')'){//不断弹出操作符号知道遇到'('while(operate.top() != '('){process.append(operate.pop());}operate.pop();}else if((rawadd[i] == '&') || (rawadd[i] == '|') || (rawadd[i] == '*') || rawadd[i] == '+' || rawadd[i] == '?'){if(!operate.empty()){QChar t = operate.top();if(Priority(t) >= Priority(rawadd[i]))//如果栈内的运算符优先级大于等于当前运算符,则弹出栈内运算符{process.append(operate.pop());}}operate.push(rawadd[i]);//把当前运算符入栈}if(i == length-1)//读完字符串,把剩下的符号全部弹出{while(!operate.empty()){process.append(operate.pop());}}i++;}
NFA,就是基本结构,由两个点一条边构成。 相比于DFA,NFA就是多加空的边。这样方便合并,不会过于复杂。
正则表达式转成后缀之后,就可以开始构建图结构。
以邻接表形式存储NFA图
后缀表达式,按照扫描的方式来做:
基本NFA图:
是两个结点一条边。
之后构图就是由这些基本结点连接,形成图。
而连接这些基本的结点的就是空,一条边。
手工做NFA图,以及状态转换表
首先知道运算符号的优先级:
最高优先级: 闭包( * ),正闭包( + ),? (可选符号)
第二级:连接( & )
第三级: 选择( | )
第四级: 右括号(( )
一、 连接运算符: a&b
节点编号就是按顺序,这边编号可以忽略。
else if(process_r[i] == '&'){NFA* s = nullptr;NFA* e = nullptr;if(!nfa_stack.empty()){e = nfa_stack.pop();}if(!nfa_stack.empty()){s = nfa_stack.pop();}NFA* new_n = link_table->addNFA(s, e);nfa_stack.push(new_n);}
NFA* Linklist::addNFA(NFA* s, NFA* e){//空串连接两个状态Linknode* a = new Linknode(e->start->stateNum, '^');vertexs[s->end->stateNum]->next = a;e->start->worker = '^';//压入新的NFA部分NFA* new_n = new NFA(s->start, e->end);return new_n;
}
在邻接表中,需要新的节点来连接,所以新创建了一个节点。
二、选择运算符号 : a|b
or (a|b)
转成后缀之后,变成了 ===== > ab|
所以先生成两个基本NFA结构。
然后扫描到选择符号。设计一个函数来完成。
函数参数是两个NFA图,包括每一个的初态和终态。用于连接操作。
返回值: 一个合并后的NFA图
选择符号需要加两个节点,一个初态节点,一个终态节点。
初态节点要加两条边连接两个NFA图的初态。
两个NFA图的终态要各出一条边连接到新终态节点。
便于理解的图。
实际构建的图如下:
下图中紫色的就是需要添加的部分
加工后:加“^”,也是为了后面代码画图的时候,分辨出来是伊普西隆。
选择运算也是一样,需要把所有的初态和终态都新建立,连接到邻接表中。
NFA* Linklist::orNFA(int& snum, NFA *s, NFA *e){//新的初态Linknode* new_s = new Linknode(snum++, '#');vertexs.append(new_s);//加入邻接表数组//新初态连接两部分s->start->worker = '^';e->start->worker = '^';Linknode* new_etable = new Linknode(e->start->stateNum, e->start->worker);Linknode* new_stable = new Linknode(s->start->stateNum, s->start->worker);new_stable->next = new_etable;new_s->next = new_stable;//新的终态Linknode* new_e = new Linknode(snum++, '^');vertexs.append(new_e);//加入邻接表数组//新终态连接两部分Linknode* new_etable2 = new Linknode(new_e->stateNum, new_e->worker);vertexs[s->end->stateNum]->next = new_etable2;Linknode* new_etable3 = new Linknode(new_e->stateNum, new_e->worker);vertexs[e->end->stateNum]->next = new_etable3;//压入新的NFA部分NFA* new_n = new NFA(new_s, new_e);return new_n;
}
三、闭包运算符号 : a*
a的0-n个连接。
![在这里插入图片描述](https://img-blog.csdnimg.cn/7166b9817eee474896161adf9d8c258f.png
NFA* Linklist::closureNFA(int& snum, NFA* s)
{//新的初态并连接Linknode* new_s = new Linknode(snum++, '#');s->start->worker = '^';Linknode* new_stable = new Linknode(s->start->stateNum, s->start->worker);new_s->next = new_stable;vertexs.append(new_s);//加入邻接表数组//新的终态并连接Linknode* new_e = new Linknode(snum++, '^');vertexs.append(new_e);//加入邻接表数组Linknode* new_etable = new Linknode(new_e->stateNum, '^');Linknode* new_etable2 = new Linknode(new_e->stateNum, '^');vertexs[s->end->stateNum]->next = new_etable;//旧终态连接new_stable->next = new_etable2;//新初态连接//旧初态和旧终态的连接Linknode* new_stable2 = new Linknode(s->start->stateNum, s->start->worker);new_etable->next = new_stable2;//压入新的NFA部分NFA* new_n = new NFA(new_s, new_e);return new_n;
}
四、圆括号 : ( 1- 10)
圆括号的作用就是,把一堆东西当作一个整体。后缀表达式中不包括括号,
例子:
源字符串: a|b*[1-3]a*
加了&之后:a|b*&(1|2|3)&a*
变成后缀之后:ab12|3|&a&|
现在用后缀生成NFA状态表和图结构
这里定义一个NFA类,NFA有两个指针,分别指向初态和终态。
只做记录,不做连接。 其实是为了函数传参。做连接的还是Linktable,其中是带有指针的节点作为组成部分。这里用Linktable 来完成。具体结构如下:
NFA只是为了传递参数。 重要的还是 Linklist* link_table = new Linklist();
class Linklist
{public:Linklist();QVector<Linknode*> vertexs;}
五、问号 : (?)
以 a?为例:
else if(process_r[i] == '?'){NFA* s = nullptr;if(!nfa_stack.empty()){s = nfa_stack.pop();}NFA* new_n = link_table->seleNFA(state_num, s);nfa_stack.push(new_n);}
NFA* Linklist::seleNFA(int &snum, NFA *s)
{//新的初态并连接Linknode* new_s = new Linknode(snum++, '#');s->start->worker = '^';Linknode* new_stable = new Linknode(s->start->stateNum, s->start->worker);new_s->next = new_stable;vertexs.append(new_s);//加入邻接表数组//新的终态并连接Linknode* new_e = new Linknode(snum++, '^');vertexs.append(new_e);//加入邻接表数组Linknode* new_etable = new Linknode(new_e->stateNum, '^');Linknode* new_etable2 = new Linknode(new_e->stateNum, '^');vertexs[s->end->stateNum]->next = new_etable;//旧终态连接new_stable->next = new_etable2;//新初态连接//压入新的NFA部分NFA* new_n = new NFA(new_s, new_e);return new_n;
}
六、正闭包,(+)
else if(process_r[i] == '+'){NFA* s = nullptr;if(!nfa_stack.empty()){s = nfa_stack.pop();}NFA* new_n = link_table->opClosureNFA(state_num, s);nfa_stack.push(new_n);}
NFA* Linklist::opClosureNFA(int& snum, NFA* s)
{//新的初态并连接Linknode* new_s = new Linknode(snum++, '#');s->start->worker = '^';Linknode* new_stable = new Linknode(s->start->stateNum, s->start->worker);new_s->next = new_stable;vertexs.append(new_s);//加入邻接表数组//新的终态并连接Linknode* new_e = new Linknode(snum++, '^');vertexs.append(new_e);//加入邻接表数组Linknode* new_etable = new Linknode(new_e->stateNum, '^');vertexs[s->end->stateNum]->next = new_etable;//旧终态连接//旧初态和旧终态的连接Linknode* new_stable2 = new Linknode(s->start->stateNum, s->start->worker);new_etable->next = new_stable2;//压入新的NFA部分NFA* new_n = new NFA(new_s, new_e);return new_n;
}
邻接表中的邻接点都是新建立的。 在vector 数组中不能进行连接,所以代码会比较复杂。
所有的连接都是创建新的节点,和新的节点连接。
下面的代码中, new_stable ,new_stable2, new_etable, new_etable2都是在链表后面加邻接点。
例: vertexs[s->end->stateNum]->next = new_etable2;
正则表达式-》NFA完成啦(以上的方法有个名字: Thompson方法)
下一步:NFA->DFA
思路就是: 1. epsilon 转换。2.多重转换(去掉一对多)
1. epsilon 转换
epsilon连接的两个点可以理解为等价的状态。
** 手工做法:穷举所有可能的情况**
由节点1作为起点,
看有几个字母,状态转换表就有几列。
步骤2中有A,B,C,D四种情况,其中A是初始情况(ε _ c l o s u r e ( 0 ) = { 0 , 1 , 2 , 4 , 7 } ε_closure(0)={0,1,2,4,7}ε_closure(0)={0,1,2,4,7}. 多说几句,在NFA图1中,是从0出发的,所以初始情况是ε _ c l o s u r e ( 0 ) ε_closure(0)ε_closure(0),那假设如果是从X开始,则初始情况是ε _ c l o s u r e ( X ) ε_closure(X)ε_closure(X))
手工做法结束。代码思路如下:
思路
DFA也是一个图。使用dfa_first表示。
DFA图,用一个新的结构体表示。
因为有可能存在等价状态。所以用一个集合表示。
struct dfaNode{QSet<int> statu_set;QChar worker = '#';dfaNode* next = nullptr;
};
可以看的状态转换表格,状态部分,由1-n个节点等价。
第一步:完成找到和本节点以epsilon连接的点的一个集合。
每次都是从状态0 开始。
初始化 DFA
void MainWindow::iniDFAFirst()
{link_table->first = nfa_stack.top()->start; //获取初态link_table->dfa_first = new dfaNode();link_table->dfsEpsilon(link_table->first, link_table->dfa_first->statu_set);//寻找初态的等价状态link_table->dfalist.push_back(link_table->dfa_first);
}
注意:
这里的dfa 先使用初态为链表头。 进行初始化。
比如 a*
获得的第一个状态是2, 并且和0,3都是epsilon连接的。所以第一个DFA结点 中字符集合是{2,0,3}
本步骤:找到与0 等价的所有结点,并且把该状态加入到集合中。
void Linklist::dfsEpsilon(Linknode* p, QSet<int>& f){while(p != nullptr){if(p->worker == '^' || p->worker == '#'){f.insert(p->stateNum);if(p->next != nullptr)dfsEpsilon(vertexs[p->next->stateNum], f);}else {return; }p = p->next;}}
经过字符 QChar c之后到达的状态加入到集合s 中
void Linklist::dfsChar(dfaNode* p, QChar c, QSet<int>& s)
{QVector<int> t;QSet<int>::iterator iter;for(iter = p->statu_set.begin(); iter != p->statu_set.end(); iter++){if(s.contains(vertexs[*iter]->stateNum)) {//不重复添加continue;}Linknode* a = vertexs[*iter]->next;//查找在该状态中的所有结点的邻接点while(a != nullptr){if(a->worker == c && !vectorfind2(t, vertexs[*iter]->stateNum))//如果是通过一个字符c到达的结点则放入t{t.push_back(a->stateNum);if(vertexs[a->stateNum]->next!=nullptr) dfsEpsilon(vertexs[vertexs[a->stateNum]->next->stateNum], s);}a = a->next;}}//已有的集合经过一个字符c能到达的点也需要加入集合QSet<int>::iterator iter1;for(iter1 = s.begin(); iter1 != s.end(); iter1++){Linknode* a = vertexs[*iter1]->next;while(a != nullptr){if(a->worker == c){s.insert(a->stateNum);}a = a->next;}}for(int i=0;i<t.size();i++)//遍历所有a状态能通过字符c到达的结点{Linknode* a = vertexs[t[i]]->next;//遍历这些结点的邻接点while(a != nullptr){if(a->stateNum == 11) qDebug()<<"11";dfsEpsilon(vertexs[a->stateNum], s);//把通过一个字符c能到达的结点的能通过epsilon到达的结点加入集合sa = a->next;}}VectorToSet(t, s);//把经过一次字符c的结点中被选择的放入
}
转换成DFA的部分
void MainWindow::iniDFAFirst()
{link_table->first = nfa_stack.top()->start; //获取初态link_table->dfa_first = new dfaNode();link_table->dfsEpsilon(link_table->first, link_table->dfa_first->statu_set);//寻找初态的等价状态link_table->dfalist.push_back(link_table->dfa_first);
}
void MainWindow::toDFA()
{iniDFAFirst(); //初始化表头结点for(int j=0;j<link_table->dfalist.size();j++){for(int i = 0;i<alphabet.size();i++){dfaNode* d = new dfaNode();link_table->dfsChar(link_table->dfalist[j], alphabet[i], d->statu_set);if(d->statu_set.size() != 0){d->worker = alphabet[i]; //经过的字符 alphabet[i]d->next = link_table->dfalist[j]->next;link_table->dfalist[j]->next = d;}else{delete d;continue;}dfaNode* new_d = new dfaNode();new_d->statu_set = d->statu_set; //复制一份 d 的信息,不能直接连接到邻接表中new_d->worker = d->worker;if(new_d->statu_set.size() != 0 && link_table->dfalist[j]->statu_set != new_d->statu_set && !Vectorfind(link_table->dfalist, new_d))link_table->dfalist.push_back(new_d);}}
}
转成表格
void MainWindow::DFAtoForm()
{QStandardItemModel * model = new QStandardItemModel();//设置列model->setColumnCount(alphabet.size()+1); //设置列数model->setHorizontalHeaderItem(0, new QStandardItem("状态"));for(int i=1;i<=alphabet.size();i++){model->setHorizontalHeaderItem(i, new QStandardItem(QString(alphabet[i-1])));for(int j=0;j<link_table->dfalist.size();j++){dfaNode* p = link_table->dfalist[j]->next;QString state_set = "";while(p != nullptr){if(p->worker == alphabet[i-1]){QSet<int>::iterator iter1;for(iter1 = p->statu_set.begin(); iter1 != p->statu_set.end(); iter1++){state_set.append(" " + QString::number(*iter1) + ",");}}p = p->next;}model->setItem(j, i, new QStandardItem(state_set));}}//设置状态列for(int i=0;i<link_table->dfalist.size();i++){dfaNode* p = link_table->dfalist[i];QString state_set = "";QSet<int>::iterator iter1;for(iter1 = p->statu_set.begin(); iter1 != p->statu_set.end(); iter1++){state_set.append(" " + QString::number(*iter1) + ",");}model->setItem(i, 0, new QStandardItem(state_set));//第一列的设置。把所有状态列出。}ui->tableView_4->setModel(model);
}
下面介绍DFA最小化
借用一个博主的
解题过程如下:
1.根据状态转换图写出状态转换表
算法思路
1.先把所有状态放入一个vector, 通过查找dfaNode的集合是否相同来获取对应的下标作为新的状态号
QVector<dfaNode*> v;for(int i=0;i<dfalist.size();i++){dfaNode* d = new dfaNode();d->statu_set = dfalist[i]->statu_set;v.push_back(d);}
- 遍历邻接表, 替换所有的状态集合为新的状态号
for(int i=0;i<dfalist.size();i++){//形成mindfa邻接表数组dfaNode* p = dfalist[i];minDfaNode* f = new minDfaNode();int index;if(vectorfind(v, p, index))//获取新的状态号{f->state_num = index;f->worker = p->worker;if(p->statu_set.contains(vertexs.size()-1)) f->finish = true;//标记是否是终态,用以划分mindfalist.push_back(f);}//形成mindfa邻接表的邻接点p = p->next;while(p != nullptr){int index2;if(vectorfind(v, p, index2))//获取新的状态号{minDfaNode* k = new minDfaNode();k->state_num = index2;k->worker = p->worker;if(p->statu_set.contains(vertexs.size()-1)) k->finish = true;//标记是否是终态,用以划分k->next = f->next;f->next = k;}p = p->next;}}
找状态号的函数
bool vectorfind(QVector<dfaNode*> v, dfaNode* d, int& index)
{for(int i=0;i<v.size();i++){if(d->statu_set == v[i]->statu_set){index = i;return true;}}index = -1;return false;
}
正则表达式转NFA,DFA,最小化DFA相关推荐
- NFA、DFA模拟、正则表达式转NFA、NFA转DFA、DFA转正则、DFA最小化的python实现项目
各类自动机模拟实现 项目地址: https://github.com/HuiyuanYan/automaton_simulation 注:这个github链接必须复制重新在浏览器打开,不能通过CSDN ...
- 编译原理: 最小化 DFA(划分) 验证 DFA(Kleene 闭包)
编译原理: 最小化 DFA(划分) & 验证 DFA(Kleene 闭包) 文章目录 编译原理: 最小化 DFA(划分) & 验证 DFA(Kleene 闭包) 简介 参考 正文 示例 ...
- 正则表达式引擎的构建——基于编译原理DFA(龙书第三章)——5 DFA最小化
完整引擎代码在github上,地址为:https://github.com/sun2043430/RegularExpression_Engine.git DFA最小化的算法原理 "DFA状 ...
- 正规式到最小化DFA
整体的步骤是三步: 一.先把正规式转换为NFA(非确定有穷自动机) 二.在把NFA通过"子集构造法"转化为DFA 三.在把DFA通过"分割法"进行最小化 一.正 ...
- 【编译原理】最小化 DFA
最小化 DFA 最小状态DFA的含义 1.没有无关状态(多余状态.死状态) 什么是无关状态? 死状态:从这个状态没有通路到达终态:S1 多余状态:从开始状态出发,任何输入串也不能到达的那个状态.S2 ...
- 编译原理学习笔记(十五)~最小化DFA
概念 最小化:优化DFA,使其状态数最少. 那么什么时候状态数是最少的呢?这里我们需要介绍两个新的名词:可区分和不可区分. 官方定义: 可区分:对于任何两个状态t和s,若从一状态出发接 ...
- 【编译原理】— 求最小化DFA
(编译原理) 求最小化DFA(写出所有过程) 解题过程如下: 1.根据状态转换图写出状态转换表 2.写出DFA表 I Ia Ib A B C B B D C B C D B E E B C 3.画出D ...
- python实现dfa过滤算法_Hopcroft算法DFA最小化Python实现
DFA最小化原理 所谓自动机的化简问题即是对任何一个确定有限自动机DFA M,构造另一个确定有限自动机DFA M',有L(M)=L(M'),并且M'的状态个数不多于M的状态个数,而且可以肯定地说,能够 ...
- nfa确定化 dfa最小化_深度学习中的优化:梯度下降,确定全局最优值或与之接近的局部最优值...
深度学习中的优化是一项极度复杂的任务,本文是一份基础指南,旨在从数学的角度深入解读优化器. 一般而言,神经网络的整体性能取决于几个因素.通常最受关注的是网络架构,但这只是众多重要元素之一.还有一个常常 ...
最新文章
- 数据结构(1)有序表查找
- Android 图片放错位置会拉伸变形
- 设置树莓派SSH连接因超时闲置断开(转)
- python有必要学吗-Python这么火,要不要学?听听华为工程师怎么说...
- sql中union 和 union all的区别
- CM: 如何通过table SKWG_BREL快速查询product attachment信息
- C .Adding Powers codeforces(位运算思维)
- db2 获取返回的游标_MySQL ------ 存储过程与游标简单使用
- 利用dos进入mysql数据库操作数据
- java如何简单的将一个三位正整数分解成三个数
- java面向对象_谈谈Java的面向对象
- [转]跟我一起写Makefile系列
- Cmailserver和outlook配置
- 浅谈JAVA项目开发
- #(最新最全)PDB(Protein Data Bank)数据格式详解
- AI智能语音识别计算器
- 人工智能专业志愿该如何填报?
- [Kaldi] MFCC特征提取源码详解
- 将数据源的数据格式化显示,加上金额符号
- 对话深喉:中小App如何突围?(开发者必看)