1. 程式人生 > >用有限自動機實現正則表示式的匹配

用有限自動機實現正則表示式的匹配

問題:在主串中查詢是否存在正則表示式為 abc*d?e 的匹配,下面用有限自動機的方法查詢,該正則表示式的最簡 DFA 如下

狀態轉換表如下圖所示

程式中用二維陣列定義如下

#define STATES_NUMBER	5
#define LETTER_NUMBER	5

// 表示 abc*d?e 的最簡 DFA 的狀態轉換表(-1表示不接受)
int trans_table[STATES_NUMBER][LETTER_NUMBER] = {
	1, -1, -1, -1, -1,
	-1, 2, -1, -1, -1,
	-1, -1, 2, 3, 4,
	-1, -1, -1, -1, 4,
	-1, -1, -1, -1, -1
};
演算法是暴力匹配,其中 is_matched 函式模仿了 C 語言中的庫函式 strstr(Linux 下的原始碼,魯棒性高)。

程式如下

/**************************************************************************
created:	2014/03/08
filename:	main.c
author:		Justme0(http://blog.csdn.net/justme0)

purpose:	模擬 DFA,在主串中搜索模式 abc*d?e,查詢是否存在匹配
**************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#define MAX_LENGTH		100
#define STATES_NUMBER	5
#define LETTER_NUMBER	5

// 表示 abc*d?e 的最簡 DFA 的狀態轉換表(-1表示不接受)
// 查詢能否匹配上,無需貪婪匹配,故作為接受狀態的最後一行實際上不會被訪問到
int trans_table[STATES_NUMBER][LETTER_NUMBER] = {
	1, -1, -1, -1, -1,
	-1, 2, -1, -1, -1,
	-1, -1, 2, 3, 4,
	-1, -1, -1, -1, 4,
	-1, -1, -1, -1, -1
};

/*
** 是否是接受狀態
*/
int is_end(const int state) {
	return STATES_NUMBER - 1 == state;
}

/*
** state 是否接受 letter
*/
int is_acceptable(const int state, const char letter) {
	int column = letter - 'a';
	assert(0 <= state && state < STATES_NUMBER);
	if (!(0 <= column && column < LETTER_NUMBER)) {
		return 0;
	}
	
	return -1 != trans_table[state][column];
}

int move(const int state, const char letter) {
	int column = letter - 'a';
	
	return trans_table[state][column];	// is_acceptable 與 move 有冗餘,待改進
}

/*
** 若主串中匹配上了模式則返回1,否則返回0
*/
int is_matched(const char *const str) {
	const char *head = NULL;	// head 是當前模式的頭在 str 中的位置
	const char *p = NULL;		// p 指示主串
	int state = 0;				// state 代表模式
	
	for (head = str; '\0' != *head; ++head) {
		state = 0;
		for (p = head; '\0' != *p && (!is_end(state))
			&& is_acceptable(state, *p); ++p) {
			state = move(state, *p);
		}
		
		if (is_end(state)) {
			return 1;
		}
	}
	
	return 0;
}

int main(int argc, char **argv) {
	char str[MAX_LENGTH];
	int ans;
	
	FILE * in_stream = freopen("test.txt", "r", stdin);
	if (NULL == in_stream) {
		printf("Can not open file!\n");
		exit(1);
	}
	
	while (EOF != scanf("%s", str)) {
		scanf("%d", &ans);
		assert(ans == is_matched(str));

		if(is_matched(str)) {
			printf("找到 abc*d?e 匹配\n");
		} else {
			printf("沒有找到 abc*d?e 匹配\n");
		}
	}

	fclose(in_stream);
	
	return 0;
}

/*test.txt
abe 1
abee 1
abde 1
eabcce 1
bb33_aabcabdee 1
-*+68abcdababaabcccccccdeeeesabc 1
a 0
abc 0
b 0
. 0
eab 0
eabcccd 0
abdff 0
&*%&(* 0
*/

有幾點感受:

1、ACM 中常常用到的 AC 自動機與這個應該有區別,那個常用 Trie 樹實現。

2、上面也提到了,用的是暴力匹配,也就是說此次沒匹配上,模式向前移動一個字元,又重新匹配,我想應該有類似 KMP 的演算法,沒匹配上可滑動多個字元。

C++11 中加入了正則表示式,程式如下:

/********************************************************************
created:	2014/03/09 0:51
filename:	test.cpp
author:		Justme0 (http://blog.csdn.net/justme0)

purpose:	正則匹配
*********************************************************************/

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
#include <regex>
#include <cassert>
using namespace std;

int main() {
	FILE * in_stream = freopen("test.txt", "r", stdin);
	if (NULL == in_stream) {
		printf("Can not open file!\n");
		exit(1);
	}

	regex rgx("abc*d?e");
	string text;
	bool ans;

	while (cin >> text) {
		cin >> ans;
		assert(ans == regex_search(text, rgx));

		if(regex_search(text, rgx)) {
			printf("找到 abc*d?e 匹配\n");
		} else {
			printf("沒有找到 abc*d?e 匹配\n");
		}
	}

	fclose(in_stream);

	return 0;
}

regex 庫能滿足多種正則匹配的需求,上面的 regex_search 用法是最簡單的一種,返回布林值,查詢是否存在匹配。

20140310

這兩天查了資料,發現上面所說的第二個問題是可以解決的,原理與 KMP 類似,得把 DFA 修改一下,只需對主串掃一遍,讓它在 DFA 上跑。修改後的 DFA 如下,other 指某狀態未標出的所有其他符號。

狀態轉換表

程式如下,時間複雜度 O(n),n 為主串長度

/**************************************************************************
created:	2014/03/10
filename:	main.c
author:		Justme0 (http://blog.csdn.net/justme0)

purpose:	模擬 DFA,在主串中搜索模式 abc*d?e,查詢是否存在匹配
			只需對主串掃一遍
**************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#define MAX_LENGTH		100
#define STATES_NUMBER	5
#define LETTER_NUMBER	6

// 查詢能否存在匹配,故無需貪婪匹配,作為接受狀態的最後一行實際上不會被訪問到
int trans_table[STATES_NUMBER][LETTER_NUMBER] = {
	1, 0, 0, 0, 0, 0,
	1, 2, 0, 0, 0, 0,
	1, 0, 2, 3, 4, 0,
	1, 0, 0, 0, 4, 0,
	0, 0, 0, 0, 0, 0
};

/*
** state 是否是接受狀態
*/
int is_end(const int state) {
	return STATES_NUMBER - 1 == state;
}

int move(const int state, const char letter) {
	int column;
	if ('a' <= letter && letter <= 'e') {
		column = letter - 'a';
	} else {
		column = LETTER_NUMBER - 1;
	}
	
	assert(0 <= state && state < STATES_NUMBER - 1);	// 最後一行不應該被執行到
	assert(0 <= column && column < LETTER_NUMBER);
	
	return trans_table[state][column];
}

/*
** 若主串中匹配上了模式則返回1,否則返回0
*/
int is_matched(const char *const text) {
	int state = 0;
	const char *p = NULL;	// p 指示輸入字元
	
	for (p = text; '\0' != *p && (!is_end(state)); ++p) {
		state = move(state, *p);
	}
	if (is_end(state)) {
		return 1;
	}
	
	return 0;
}


int main(int argc, char **argv) {
	char text[MAX_LENGTH];
	int ans;
	int cnt = 1;
	
	FILE * in_stream = freopen("test.txt", "r", stdin);
	if (NULL == in_stream) {
		printf("Can not open file!\n");
		exit(1);
	}
	
	while (EOF != scanf("%s", text)) {
		scanf("%d", &ans);
		assert(ans == is_matched(text));	// 用於測試
		
		printf("Case %d: ", cnt++);
		if(is_matched(text)) {
			printf("找到 abc*d?e 匹配\n");
		} else {
			printf("沒有找到 abc*d?e 匹配\n");
		}
	}
	
	fclose(in_stream);
	
	return 0;
}

/*test.txt
abe 1
abee 1
abde 1
eabcce 1
bb33_aabcabdee 1
-*+68abcdababaabcccccccdeeeesabc 1
a 0
abc 0
b 0
. 0
eab 0
eabcccd 0
abdff 0
&*%&(* 0
*/