字符串匹配算法总结(RobinKarp、KMP、后缀数组)

字符串匹配算法总结(RobinKarp、KMP、后缀数组)

这里主要整理了这三种常见的字符串匹配算法:① RobinKarp ② KMP ③ 后缀数组

一、RobinKarp算法(hash -> 滚动hash)

使用例子进行说明:

string S = “ABAAB”;

string p = “BAB“;

这里 S 是原串,p 是模式串

首先我们对模式串 p 进行hash,我们采用以下方法:

​ hash§ = (C0 × 31 + C1) × 31 + C2

这里的 31 不是固定的,31 是素数,也可以选择其他素数

如果觉得结果可能会越界,一定要记着进行取余。

加下来我们对原串 S 进行遍历,从每个位置开始进行 n = 3 长度的 hash 后开始比较。
在这里插入图片描述
到这里匹配算是完成了,但是我们来看看这样做的时间复杂度是怎么样的。

首先进行 hash 的时间复杂度为 O(n),对原串进行遍历的时间复杂度为 O(m)。

因此整体的时间复杂度为 O(mn),这对于暴力算法来说是没有什么提升的。

因此 RabinKarp 加了点预处理的味道,将复杂度降为 O(m+n)

使用 hash 数组来保存原串 S 上每位开始进行 hash 的结果。

hash[0] = C0 × 312 + C1 × 31 + C2

hash[1] = hash[0] - C0 × 312 + C3

……

这样一来在计划完 hash§ 之后再扫一遍 hash 数组进行比对就可以得到结果。

但是这个算法中使用 hash 可能会有冲突!可能还需要另外的判断,比如逐位比对,

因此时间实际上 > O(m+n)。

该算法在工业上并不常使用,但是可以用来检测抄袭,因为可以处理多模式匹配

二、KMP算法(最常见)

利用模式串的前后缀特点创建 next 数组,利用 next 数组在原串中进行单向扫描就可以得到匹配结果。

时间复杂度为 O(m+n)。
在这里插入图片描述

三、后缀数组

什么是后缀数组?看下图。
在这里插入图片描述
将所有的后缀字符串取出来按照字典序进行排序就得到了后缀数组。

实际上后缀数组(suffix array)是排名 rank原始下标 index 的映射

比如上图中 sa[0] = 0, sa[1] = 2, sa[2] = 4, sa[3] = 1, sa[4] = 2

有了后缀数组之后我们该如何进行匹配呢?

由于我们知道了后缀数组,这使得所有的后缀字符串按照字典序排序,因此可以使用二分法来匹配。

参考代码:

// 通过后缀数组匹配 
int suffixFind(string str, string subStr){
	int strLen = str.length();
	int subLen = subStr.length();
	Suffix *sa = new Suffix[strLen];
	// 获得后缀数组 
	getSuffixArray(sa, str);
	int left = 0;
	int right = strLen;
	int mid = 0;
	// 二分查找 
	while(left <= right){
		mid = (left+right)/2;
		string temp;
		// 如果长度大于模式串,则截取前缀 
		if(sa[mid].str.length() > subLen){
			temp = sa[mid].str.substr(0, subLen);
		}
		else{
			temp = sa[mid].str;
		}
		if(temp == subStr){
			return sa[mid].index;
		}
		else if(temp > subStr){
			right = mid-1;
		}
		else{
			left = mid+1;
		}
	}
	delete []sa;
	return -1;
}

那么到底如何求得后缀数组呢?

① 暴力:使用结构体 Suffix 封装后缀字符串 str 以及对应的下标 index,在截取原串中所有的后缀字符串之后形成 n 个Suffix,再直接调用 STL 库中的 sort 函数即可。时间复杂度为 O(n2lgn)。

参考代码:

// 结构体封装后缀串以及原始下标 并且定义比较规则 
struct Suffix{
	string str;
	int index;
	bool operator < (Suffix other) const{
		return str < other.str;
	}
};

// 直接用结构体排序 O((n^2)logn) 
void getSuffixArray(Suffix *sa, string str){
	// 创建sa数组 
	for(int i = 0; i < str.length(); i++){
		string sub = str.substr(i);
		sa[i].str = sub;
		sa[i].index = i;
	}
	// 排序 
	sort(sa, sa+str.length());
	for(int i = 0; i < str.length(); i++){
		cout << "str: " << sa[i].str << " index: " << sa[i].index << endl;
	}
}

倍增法:空间换时间,需要使用辅助数组 rank 来进行。

rank 数组的含义与 sa 后缀数组的含义相反,rank 数组时原始下标 index排名 rank 之间的映射。

在上面的例子中 rk[0] = 0, rk[1] = 3, rk[2] = 1, rk[3] = 4, rk[4] = 2

因此 rk[sa[i]] = i, sa[rk[i]] = i

倍增法的基本思路:

1.当前以 k = 1 为长度在原串中进行截取字符串,并且进行排序,并且保存了各个字符串的 rank;

2.下一趟就进行以 2*k 为长度在原串中进行截取,并且利用上一趟保存的 rank 数组快速比较,更新 rank 数组;

3.之后反复执行步骤 2,直到 k 大于原串的长度为止。

因此可以判断这样以 2 的倍数进行增长排序的时间复杂度为 O(n(lgn)2)。
在这里插入图片描述
参考代码:

int *rk;
int change = 0;  // 用来控制sort的比较规则 
int k, strLen;

// 结构体封装后缀串以及原始下标 并且定义比较规则 
struct Suffix{
	string str;
	int index;
	bool operator < (Suffix other) const{
		if(change == 0){
			return str < other.str;
		}
		else{
			// 如果前缀相同,比较后缀 
			if(rk[index] == rk[other.index]){
				// 如果没有越界则调用上一轮的rank数组来得到结果 
				if(index+k/2 < strLen && other.index+k/2 < strLen){
					return rk[index+k/2] < rk[other.index+k/2];
				}
				// 否则根据字符串的长度就可以得到结论 
				else{
					return str.length() < other.str.length();
				}
			}
			// 如果前缀不同直接返回结果 
			else{
				return rk[index] < rk[other.index];
			}
		}
	}
};

// 使用倍增法 O(n(logn)^2) 
void getSuffixArray(Suffix *sa, string str){
	// 创建rk排名数组 
	rk = new int[str.length()];
	// 用长度1开始,不断倍增 
	for(int i = 1; i/2 <= str.length(); i *= 2){
		k = i;
		// 重新构造sa数组 
		for(int j = 0; j < str.length(); j++){
			string sub;
			if(j+i-1 < str.length()){
				sub = str.substr(j, i);
			}
			else{
				sub = str.substr(j);
			}
			sa[j].str = sub;
			sa[j].index = j;
		}
		// 长度为1则直接sort 
		if(i == 1){
			change = 0;
			sort(sa, sa+str.length());
		} 
		// 否则改变sort规则 
		else{
			change = 1;
			sort(sa, sa+str.length());
		}
		// 更新rank数组 
		int rank = 0;
		rk[sa[0].index] = 0;
		for(int j = 1; j < str.length(); j++){
			if(sa[j].str == sa[j-1].str){
				rk[sa[j].index] = rank;
			}
			else{
				rk[sa[j].index] = ++rank;
			}
		}
	}
	delete []rk;
}

【END】感谢观看

发布了44 篇原创文章 · 获赞 17 · 访问量 9119

猜你喜欢

转载自blog.csdn.net/qq_41765114/article/details/88378904