字符串算法:正则表达式原理及C++实现_JZX555的博客-程序员秘密

技术标签: 算法  字符串算法  正则表达式  

前言

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

原理

正则表达式
首先我们需要知道什么是正则表达式。正则表达式,又称规则表达式。(英语: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将会如下所示:


代码如下所示:

/* NFA构建函数:根据正则表达式构建对应的NFA
 * 参数:regexp:用于构建NFA的正则表达式
 * 返回值:无
 */
void RegularExp::NFA(string regexp) {
	stack <int> 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 <iostream>
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 <iostream>
#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 <iostream>
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 <iostream>
#include <string>
#include <stack>
#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 <int> 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;
}

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

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



版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_41427400/article/details/80958777

智能推荐

Delphi线程定时器_weixin_30875157的博客-程序员秘密

(* 自己编写的线程计时器,没有采用消息机制,很有效 Cobbler续写 不用 TTimer 的原因: 要说TTimer类的使用问题,先要说一下它响应用户定义的回调函数(OnTimer)的方法。 TTimer拥有一个HWnd类型的成员变量FWindowHandle,用于捕捉系统消息。 TTimer在Enable的情况下,每隔Inter...

【Nodejs】使用robotjs控制鼠标键盘 自动点击屏幕上指定位置的图标 实现连接wifi等操作_hzxOnlineOk的博客-程序员秘密

每天上班开机挺麻烦,要手动连wifi;因此可以写一个很简单的自动执行脚本,执行 node xxxxxx.js安装。

Visual C++ 基本原理 _pcliuguangtao的博客-程序员秘密

<br />  Visual C++ 基本原理 收藏 <br />Visual C++ 基本原理 <br />// Name: Easyright<br />// Date: 8-1-2003<br />// Homepage: http://www.easyright.net<br />// Email: [email protected]<br /><br />FAQ<br />问:阅读以下文章需要具备哪些知识?<br />答:只要会开机就行了,如果大家有C++和面向对象(Object-Orie

JDK删除不彻底重装不了怎么办_win11jdk卸载后无法安装_撒木木的博客-程序员秘密

JDK删除不彻底重装不了怎么办我们有时候需要卸载JDK重装,而在卸载JDK重装的过程中会提示JDK已存在,但是我们明明环境和本地都删掉了。这个情况是因为,我们安装了JAVA JDK之后,会有两个程序,需要删掉才可以。步骤1. 点击“开始”,下拉到“Windows系统”,找到“控制面板”,点开。2. 点击卸载程序。 3. 找到图片标识的两个程序,右键卸载,搞定。...

Help Bubu - UVa 12235 四维状压dp_提比-我有特殊的AC技巧的博客-程序员秘密

Bubu's bookshelf is in a mess! Help him!There are n books on his bookshelf. We define the mess value to be the number of segments of consecutive equal-height books. For example, if the book height

一个程序员眼里的茶行业_曦图老吴的博客-程序员秘密

有关中国茶,曾经做过些公开的演讲,比如亿邦动力的《聊聊喝茶这回事》,IC咖啡的《茶行业门口的野蛮人》,但大多数时候,是围坐在茶台前,有茶艺师泡着茶,和一圈茶友交流。真正的茶圈和收藏圈一样,其实都是相当专业的小圈子,外人很难窥得真实面目。大多数人,也就是做做谈资的二传手,装装B,有意无意的以讹传讹而已。这就增加了我们这种圈外人士学茶的难度和代价,更加不要说进入茶行业了。但是,我们程序员是不信邪的????首先,我们来了解下茶的生产过程。我们讲茶有三次生命(茶艺师常常会用女人的三次生命来做类比

随便推点

SSL2838 2017年11月6日提高组T1 矩阵(贪心)_t1数据是矩阵吗_hxxxx!的博客-程序员秘密

2017年11月6日提高组T1 矩阵Description给你一个N*M 的矩阵,矩阵里面的元素要么是正数,要么是负数,它们的绝对值不大 于10000。现在你可以对矩阵进行两种操作: 1、将某一列的元素全部取反。 2、将某一行的元素全部取反。 你可以执行任意次操作。 Task:通过以上两种操作如果可以将整个矩阵的元素全变为正数的话,则输出最少的操 作次数,否则输出“impos

raid卡组不同raid_VR Mountains and Hills Pack (Google Cardboard)加速器_weixin_39955829的博客-程序员秘密

休闲|27.4MChristmas has arrived has made the climbers excited. Christmas bells ring, beautiful song from Greja, deer fly through clouds, Christmas trees with beautiful lights, slippery ice, and snowfal...

Android Layout的layout_height等属性为什么有时会不起作用?_fei0724的博客-程序员秘密

有的时候,我们配置好的布局文件,在加载完成添加到我们的Activity中后发现,并没有安装我们设置的属性来布局,比为我们设置了android:layout_marginTop=”100dip”,但是运行程序后发现一点作用都没有,相似的还有layout_height等以android:layout_开头的属性设置都没有作用,这类问题以我们使用Adapter的作为数据源的时候作用居多,因为Adapte

微信公众号测试账号-redirect_uri域名与后台配置不一致,错误代码:10003_光辉岁月gds的博客-程序员秘密

微信公众号测试账号-redirect_uri域名与后台配置不一致,错误代码:10003进入公众平台测试账号。登录公众账号——“开发者中心”——“公众平台测试账号”。需要配置的几个重要的点:1.此处配置服务器接口拿到Token跟下面的Token一致(此处域名配置需要加http(s)前缀)2.此处是微信网页授权的服务器域名配置,需要与上面拿取Token的域名一致但如果授权是为了获取un...

FFmpeg工具(一) --- ffmpeg视频处理命令行大全_ffmpeg vf_昵称正在编辑中的博客-程序员秘密

FFmpeg是视频处理最常用的开源软件。它功能强大,用途广泛,大量用于视频网站,也是许多音视频格式的标准编解码实现。FFmpeg的使用格式ffmpeg的命令行参数非常多,可以分成五个部分。$ ffmpeg {1} {2} -i {3} {4} {5}$ ffmpeg [全局参数] [输入文件参数] -i [输入文件] [输出文件参数] [输出文件]上面命令中,参数依次为1.全局参数,2.输入文件参数,3.输入文件,4.输出文件参数,5.输出文件。比如:$ ffmpeg \-y \

【栈】逆波兰表达式求值_蒙面侠1024的博客-程序员秘密

题目:根据 逆波兰表示法,求表达式的值。有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。示例 1:输入:tokens = [“2”,“1”,"+",“3”,"*"]输出:9解释:该算式转化为常见的中缀算术表达式为:((2 + 1) *3) = 9示例 2:输入:tokens = [“4”,“13”,“5”,"/","+"]输出:6解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6示例 3:输入:tokens

推荐文章

热门文章

相关标签