字符串算法:正则表达式原理及C++实现

前言

临近期末,事情特别的多,想写一些博客也是没什么时间;最近终于考的是差不多,着手写写之前一直想写的正则表达式的原理及其实现,感觉再不写就要忘完了。

原理

正则表达式
首先我们需要知道什么是正则表达式。正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex, regexp 或 RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。(节选自百度百科)
通俗来讲,即我们可以根据给出的一定的规则,进行文本的匹配;比如我们给出关键字"JZX"或"555"那么我们即可在对应文本中进行检索,如果文本中含有"JZX"或者"555"那么我们即检索成功。那么我们先对其有了一个大致的概念后,我们就开始对其基本操作进行介绍:

1.链接操作:即所指示的东西同时出现,如"AB"即代表我们检索的文本中必须出现"AB"才行;

2.或操作:即所指示的东西,至少有一个出现,我们用符号"|"来代表或操作,如"A|B"即代表我们检索的文本中必须出现"A"或者"B"或者都出现;

3.闭包操作:闭包操作即将模式的部分重复任意的次数,我们用"*"来表示闭包操作,如"A*"表示检索文本中有0个到无穷个"A"都会检索成功;

4.括号操作:括号用来表示模式的优先级,我们使用"()"来表示,如"(AC)|B"表示检索文本中有无"AC"或"B";

说了这么多,你也可能没怎么看懂,我举几个例子吧:

表达式:(A|B)(C|D) ;匹配字符串:AC、AD、BC、BD

表达式:A(B|C)*D ;匹配字符串:AD、ABD、ACD、ABCD、ABBD、ACCD、ABCCBD.......

表达式:A*|(A*BA*BA*)* ;匹配字符串:AAA、BBAABB、BABAAA.......

非确定有限状态自动机(NFA)

在这里,我们要提出一个概念及非确定有限状态自动机(NFA),与之前博客中提到的确定有限状态自动机(DFA)类似,NFA也是一种状态机,且因为我们给出确定的模式,其状态数也是确定,但是因为我们支持或操作以及闭包操作,因此其状态是不确定的,所以其方法与KMP算法中还有所不同。在NFA中,我们将根据不同的操作,构建我们的状态机:

1.链接操作:链接操作会是整个NFA构建中最简单的部分,我们只需将其与下一个状态进行转换即可,但是我们不能在状态机中对他们进行连接

2.括号操作:因为括号操作涉及到了优先级问题,因此我们先将其与下一个状态进行链接,之后使用栈来处理它,当遇到左括号压栈,遇到右括号弹栈,并构建一条右括号到下一个状态的路径;

3.闭包操作:因为闭包操作需要进行任意次数的重复,因此我们将允许在状态机中对其进行来回往复,多次到达同一个状态,即将"*"状态与其对应的字符进行双向链接,若闭包操作对象有括号操作,那么我们应该对左括号进行双向链接,最后我们将"*"状态与下一状态进行连接;

4.或操作:或操作将是NFA构建中最难的一部分,我们将根据是否有括号,将其分为两种你处理,当没有括号时,我们将从"|"状态出进行分叉,将其链接到模式末尾,同时将起始状态链接到"|"状态的下一状态;当有括号时,我们将其链接到下一个右括号处,将左括号链接到"|"状态的下一状态;

以上即为我们所有的NFA构建操作,值得我们注意的是,我们并不会将相邻的状态之间构建通路,只有我们在处理括号、闭包以及或运算时才会产生新的路径,这是帮助理解NFA构建的至关重要的一步。

这里将给出一个例子帮助大家理解:

我们构建"((A*B|AC)D)"的NFA将会如下所示:

字符串算法:正则表达式原理及C++实现_第1张图片

代码如下所示:

/* NFA构建函数:根据正则表达式构建对应的NFA
 * 参数:regexp:用于构建NFA的正则表达式
 * 返回值:无
 */
void RegularExp::NFA(string regexp) {
	stack  ops;
	// 储存正则表达式并获取其长度
	re = regexp;
	M = regexp.length();
	// 构建新的有向图
	G = new Graph(M + 1);

	// 遍历整个正则表达式
	for (int i = 0; i < M; i++) {
		int lp = i; // 储存起始位置
		// 碰到'('或'|'则将该位置压入栈中,即进行括号配对
		if (re[i] == '(' || re[i] == '|')
			ops.push(i);

		// 括号配对
		else if (re[i] == ')') {
			int or = ops.top();
			ops.pop();
			// 处理括号中有'|'的特殊情况
			// --------------------------------------有待改进------------------------------------------------------------
			if (re[or] == '|') {
				lp = ops.top();
				ops.pop();
				// 链接或符号
				G->addEdge(lp, or +1);
				G->addEdge(or , i);
			}
			//-----------------------------------------------------------------------------------------------------------
			else
				lp = or ;
		}

		// 当其下一位为闭包操作时
		if (i < M - 1 && re[i + 1] == '*') {
			// 进行双向链接
			G->addEdge(lp, i + 1);
			G->addEdge(i + 1, lp);
		}

		// 特定符号直接连向下一位
		if (re[i] == '(' || re[i] == '*' || re[i] == ')')
			G->addEdge(i, i + 1);
	}
}

模式扫描

当我们构建好了NFA后,我们即可根据构建好的NFA进行模式扫描,但在开始之前我们需要了解到,NFA是不确定状态自动机,意味着我们当前可能处于多个不同的状态,这是有可能的,但是我们的最终目的是到达接受状态,那意味着我们完成正则表达式的识别。因此,我们在扫描的过程中,将会用到一些数据结构来暂时储存当前所有可能的状态,并在读取下一个字符的字符的时候,根据读取的字符更新状态。当然,我们还有一些特别的规则,那就是我们可以到达所有链接的路径,因为在构建NFA的时候,我们并没有将链接操作构建通路,只是将可以略过或者重复的括号操作、闭包操作以及或操作进行了路径构建,因此这将是一个路径的可达性问题;而对于链接操作,我们将会在识别下一个字符时自动进行跳转。

这是一个非常抽象的过程,建议大家可以看看《算法——第四版》中的字符串章节,将会有更清晰的认识。

代码如下:

/* 识别函数:根据以及构造好的NFA识别目标文本
 * 参数:txt:用于识别的目标文本
 * 返回值:bool:若目标文本匹配正则表达式返回tru,否则返回false
 */
bool RegularExp::Recognize(string txt) {
	// 构造Bag
	Bag pc;
	// 构造DFS搜索器
	DirectedDFS *dfs = new DirectedDFS();
	// 根据有向图G初始化DFS搜索器
	dfs->Init(*G);
	// 从第一个顶点开始进行DFS搜索
	dfs->DFS(*G, 0);

	// 依次将从第一个顶点可达顶点加入Bag
	for (int v = 0; v < G->V(); v++)
		if (dfs->Marked(v))
			pc.Insert(v);

	// 遍历整个文本
	for (int i = 0; i < txt.length(); i++) {
		// 构建匹配Bag
		Bag match;
		// 遍历所有可达顶点
		for (int v = 0; v < pc.Length(); v++)
			if (pc[v] < M)
				if (re[pc[v]] == txt[i] || re[pc[v]] == '.') // 获取所有可能的匹配并加入match
					match.Insert(pc[v] + 1);

		// 重置Bag以及DFS搜索器
		pc.MakeEmpty();
		dfs->MakeEmpty();
		// 通过有向图G初始化DFS搜索器
		dfs->Init(*G);
		// 将所有匹配文本对应位置的节点进行DFS搜索
		for (int i = 0; i < match.Length(); i++)
			dfs->DFS(*G, match[i]);

		// 依次将所有可达节点加入Bag
		for (int v = 0; v < G->V(); v++)
			if (dfs->Marked(v))
				pc.Insert(v);
	}

	// 遍历所有可达节点
	for (int i = 0; i < pc.Length(); i++)
		if (pc[i] == M) // 判断是否匹配正则表达式
			return true;
	return false;
}


上述代码中,有几个结构是自己写的:Graph,DirectedDFS,Bag三个类,分别为图类,深度优先搜索器以及包类(应该可以用Set替代),我将会给出他们的.h文件,以及完整的正则表达式文件各位可以按自己的喜好来写。

C++实现

Graph.h:
#include 
using namespace std;

// 重命名边节点,便于操作
typedef struct ArcNode *Position;

/* 边节点
 * 储存元素:
 * ArcName:该边指向的节点
 * Next:以该边的头结点为头结点的其他边
 */
struct ArcNode {
	int ArcName;
	Position Next;
};

/* 顶点节点
 * 储存元素:
 * Name:该顶点的名字
 * FirstArc:以该顶点为头结点的第一个边
 */
struct VexNode {
	int Name;
	Position FirstArc;
};

/* Graph类:有向图类
 * 接口:
 * MakeEmpty:置空整个有向图,不重置顶点只重置边
 * addEdge:向有向图中添加新的有向边
 * V:获取图中的顶点个数
 */
class Graph
{
public:
	// 构造函数
	Graph(int);
	// 析构函数
	~Graph();

	// 友元类
	friend class DirectedDFS;

	// 接口函数
	void MakeEmpty();
	void addEdge(int from, int to);

	int V();

private:
	// 数据成员
	int VexNum; // 顶点个数
	int ArcNum; // 边个数
	VexNode *AdjList; // 邻接表
};

#endif

DirectedDFS.h:

#ifndef DIRECTEDDFS_H
#define DIRECTEDDFS_H

#include 
#include "Graph.h"
using namespace std;

/* DirectedDFS类:对指定的Graph进行DFS搜索
 * 接口:
 * MakeEmpty:重置DFS搜索器
 * Init:使用指定的Graph来初始化搜索器
 * DFS:对初始化后的DFS搜索器根据指定Graph以及指定节点进行DFS搜索
 * Marked:返回搜索结果
 */
class DirectedDFS
{
public:
	// 构造函数
	DirectedDFS();
	// 析构函数
	~DirectedDFS();

	// 接口函数
	void MakeEmpty();
	void Init(const Graph &);
	void DFS(const Graph &, int);
	bool Marked(int);

private:
	// 数据成员
	bool *Table; // 储存DFS搜索结果
};

#endif

Bag.h:

#ifndef BAG_H
#define BAG_H

#include 
using namespace std;

/* Bag类:储存元素只允许存入元素,不允许删除
 * 接口:
 * MakeEmpty:重置功能,重置整个Bag
 * Length:返回Bag的大小,即储存元素个数
 * Insert:向Bag中插入元素
 * 重载:
 * []:下标运算符,返回Bag中第i个元素
 */
class Bag
{
public:
	// 构造函数
	Bag();
	// 析构函数
	~Bag();

	// 重载运算符
	int& operator [](int);

	// 接口函数
	void MakeEmpty();
	int Length();
	void Insert(int);

private:
	// 数据成员
	int *Elems; // 储存元素
	int Size; // Bag中元素个数
	int MaxSize; // Bag当前可储存元素个数
};

#endif

最后给出的将是整个ReularExpression的代码:

#ifndef REGULAREXP_H
#define REGULAREXP_H

#include 
#include 
#include 
#include "Bag.h"
#include "Graph.h"
#include "DirectedDFS.h"
using namespace std;

/* RegularExp类:正则表达式检索器
 * 接口:
 * MakeEmpty:重置整个正则表达式检索器
 * NFA:根据正则表达式构造NFA(非确定有限状态自动机)
 * Recognize:判断目标文本是否匹配正则表达式
 */
class RegularExp
{
public:
	// 构造函数
	RegularExp();
	// 析构函数
	~RegularExp();

	// 接口函数
	void MakeEmpty();
	void NFA(string);
	bool Recognize(string);

private:
	// 数据成员
	int M; // 储存正则表达式字符个数
	string re; // 储存正则表达式
	Graph *G; // 储存NFA构建的有向图
};

#endif // !REGULAREXP_H





#include "RegularExp.h"

/* 构造函数:初始化对象
 * 参数:无
 * 返回值:无
 */
RegularExp::RegularExp() {
	G = NULL;
}

/* 析构函数:对象消亡时回收储存空间
 * 参数:无
 * 返回值:无
 */
RegularExp::~RegularExp() {
	MakeEmpty();
}

/* 重置函数:重置正则表达式检索器
 * 参数:无
 * 返回值:无
 */
void RegularExp::MakeEmpty() {
	// 删除旧的NFA有向图
	delete G;
	G = NULL;
}

/* NFA构建函数:根据正则表达式构建对应的NFA
 * 参数:regexp:用于构建NFA的正则表达式
 * 返回值:无
 */
void RegularExp::NFA(string regexp) {
	stack  ops;
	// 储存正则表达式并获取其长度
	re = regexp;
	M = regexp.length();
	// 构建新的有向图
	G = new Graph(M + 1);

	// 遍历整个正则表达式
	for (int i = 0; i < M; i++) {
		int lp = i; // 储存起始位置
		// 碰到'('或'|'则将该位置压入栈中,即进行括号配对
		if (re[i] == '(' || re[i] == '|')
			ops.push(i);

		// 括号配对
		else if (re[i] == ')') {
			int or = ops.top();
			ops.pop();
			// 处理括号中有'|'的特殊情况
			// --------------------------------------有待改进------------------------------------------------------------
			if (re[or] == '|') {
				lp = ops.top();
				ops.pop();
				// 链接或符号
				G->addEdge(lp, or +1);
				G->addEdge(or , i);
			}
			//-----------------------------------------------------------------------------------------------------------
			else
				lp = or ;
		}

		// 当其下一位为闭包操作时
		if (i < M - 1 && re[i + 1] == '*') {
			// 进行双向链接
			G->addEdge(lp, i + 1);
			G->addEdge(i + 1, lp);
		}

		// 特定符号直接连向下一位
		if (re[i] == '(' || re[i] == '*' || re[i] == ')')
			G->addEdge(i, i + 1);
	}
}

/* 识别函数:根据以及构造好的NFA识别目标文本
 * 参数:txt:用于识别的目标文本
 * 返回值:bool:若目标文本匹配正则表达式返回tru,否则返回false
 */
bool RegularExp::Recognize(string txt) {
	// 构造Bag
	Bag pc;
	// 构造DFS搜索器
	DirectedDFS *dfs = new DirectedDFS();
	// 根据有向图G初始化DFS搜索器
	dfs->Init(*G);
	// 从第一个顶点开始进行DFS搜索
	dfs->DFS(*G, 0);

	// 依次将从第一个顶点可达顶点加入Bag
	for (int v = 0; v < G->V(); v++)
		if (dfs->Marked(v))
			pc.Insert(v);

	// 遍历整个文本
	for (int i = 0; i < txt.length(); i++) {
		// 构建匹配Bag
		Bag match;
		// 遍历所有可达顶点
		for (int v = 0; v < pc.Length(); v++)
			if (pc[v] < M)
				if (re[pc[v]] == txt[i] || re[pc[v]] == '.') // 获取所有可能的匹配并加入match
					match.Insert(pc[v] + 1);

		// 重置Bag以及DFS搜索器
		pc.MakeEmpty();
		dfs->MakeEmpty();
		// 通过有向图G初始化DFS搜索器
		dfs->Init(*G);
		// 将所有匹配文本对应位置的节点进行DFS搜索
		for (int i = 0; i < match.Length(); i++)
			dfs->DFS(*G, match[i]);

		// 依次将所有可达节点加入Bag
		for (int v = 0; v < G->V(); v++)
			if (dfs->Marked(v))
				pc.Insert(v);
	}

	// 遍历所有可达节点
	for (int i = 0; i < pc.Length(); i++)
		if (pc[i] == M) // 判断是否匹配正则表达式
			return true;
	return false;
}

那么整个正则表达式到这里就完了,其中我也有很多没弄好的地方,以后会不断的更在,大家如果有什么意见也请提出来,大家共同进步就好啦~~

参考文献:《算法——第四版》,百度百科



你可能感兴趣的:(算法)