算法导论第32章 字符串匹配

书中依次讲了4种方法,朴素算法、RabinKarp算法、有限自动机算法、KMP算法

1、朴素算法:

算法用一个循环来找出所有有效位移,该循环对n-m+1个可能的每一个s值检查条件P[1...m] = T[s+1....s+m];

//朴素的字符串匹配算法
void nativeStringMatcher(string st, string sp)
{
	int n = st.length();          //主串长度
	int m = sp.length();          //模式串长度

	for (int s = 0; s <= n-m+1; s++)
	{
		string ss = st.substr(s, m);   //ss为st从s位置开始的m个字符,即和sp比较的字符串
		if (sp == ss)
			cout << "在位置" << s << "处发现匹配" << endl;
	}
}
算法运行时间为O((n-m+1)m),循环运行n-m+1次,每次最多比较m次。


2、RabinKarp算法:算法思想是将字符串映射到一个数字,然后比较字串的数字是否相等,如果相等则继续进一步判断确认正确性,如果不相等,则可以判断字符串不匹配。

书中为了说明方便,采用的是十进制的数制,

已知一个模式P[1...m],设p表示其相应的十进制数的值,对于给定的文本T[1....n],用ts表示长度为m的子字符串T[s+1....s+m] (s=0,1...n-m+1)相应的十进制的值。

ts = p当且仅当T[s+1....s+m] = P[1,2...m].

可以运用霍纳法则在O(m)时间内计算p的值:  p = P[m] + 10(P[m-1] + 10(P[m-2])+....+10(P[2]+10P[1]).....)

类似的,可以在O(m)时间内根据T[1,2....m],计算出t0,

为了在O(n-m)时间内计算出剩余的值t1,t2,....tn-m,可以在常数时间内根据ts计算出t(s+1),

                                     t(s+1) = 10(ts - 10(m-1次方)* T[s+1]) + T[s+m+1]

减去 10(m-1次方)* T[s+1],就是从ts中去掉了高位数组,再把结果乘以10,就使数左移了一位,然后在加上T[s+m+1],就加入一个适当的低位数。

这要预先算出常数 10(m-1次方)(不同进制不一样,不同的映射方法也不一样)

这样可以在O(m)的时间内算出p,在O(n-m+1)时间算出t0, t1....tn-m,所以可以用O(m)的预处理时间和O(n-m+1)时间内算出匹配位置。

这个过程的问题是p和ts的值可能过大,所以可以选择一个合适的模q来计算p和ts的模。

一般情况下,采用d进制的字母表{0, 1,...d-1}时,所选取的q要满足使dq的值在一个计算机字长内,并调整递归式使其对模q进行运算,

                                                        t(s+1) = (d(ts - T[s+1]h)+  T[s+m+1])mod q

其中h = d (m-1次方)是一个m数位文本窗口中高伟数位上的数字“1”的值。

但是加入模q后,存在伪命中,还需要进一步判断。如果q足够大,伪命中就比较少,额外检查的代价就低 、

下图是关于算法的说明:


代码为:

//RK算法,映射方法
void RabinKarpMatcher(string st, string sp, int d, int q)
{
	int n = st.length();
	int m = sp.length();
	int h = 1;        //初始化为m位数字中高位数字的值
	int p = 0;        //sp字符串对应的数字的值
	int t = 0;        //st中每m个连续字符串对应的数组的值
	int i;

	//m位数字中的最高位的权值,例如10进制5位数,该值为10000
	for(i = 0; i < m-1; i++)
		h = (h * d) % q;

	//计算p值,t值
	for (i = 0; i < m; i++)
	{
		p = (p * d + (sp[i] - 'a')) % q;
		t = (t * d + (st[i] - 'a')) % q;
	}

	//开始进行匹配,如果算出的值相等,则进一步判断是否相等,
	//否则,不匹配,计算下一个m位字符串对应的值,继续循环
	//计算方法为:t(s+1) = (d*(t(s) - T[s+1]*h) + T[s+m+1]) mod q
	for (int s = 0; s < n-m+1; s++)
	{
		if (p == t && sp == st.substr(s, m))
		    cout << "在位置" << s << "处发现匹配" << endl;
		int tmp = t;
		t = (d * (tmp - (st[s]-'a')*h) + st[s+m]-'a') % q;
	}
}
虽然算法在最坏情况下时间为O((n-m+1)m),但是适当选取q可以时伪命中降低很多,运行时间可以为O(n+m)


3、有限自动机,没完全明白,,不写了

4、KMP算法:算法时间为O(n),利用了辅助数组p[1.2....m],它是在O(m)内根据模式预先算出来的,

模式的前缀字符p包含有模式与其自身的位移进行匹配的信息,这些信息可以避免在朴素的字符串匹配算法中,对无效位移进行测试

已知模式字符P[1...q]与文本字符T[s+1...s+q]匹配,那么满足  P[1...k] = T[s'+1...s'+k],其中s'+k = s+q的最小位移s' > s是多少,

对于新位移,无需把P的前k个字符与T中相应的字符进行比较,因为前面的等式已经满足了。

可以用模式与其自身进行比较,以预先计算出这些必要的信息,由于T[s'+1...s'+k]是文本中已经知道的部分,所以它是字符串Pq的一个后缀,

可以将 P[1...k] = T[s'+1...s'+k]解释为满足Pk是Pq的后缀的最大的k < q,于是,s' = s+(q-k)就是一个可能的有效位移。

以下是预计算过程的形式化说明。已知一个模式P[1...m],模式P的前缀函数是函数p{1,2...m}->{0,1,...,m-1}并满足

p[q] = max {k: k < q 且 Pk是Pq的后缀}          p[q]是Pq的真后缀P的最长前缀的长度。

以下图片帮助理解:

算法导论第32章 字符串匹配_第1张图片

代码如下:

//计算前缀数组,将模式与自身进行比较
void computePrefixFunction(string sp, int *p)
{
	int m = sp.length();
	p[0] = 0;
	int k = 0;    //匹配的字符数

	for (int q = 1; q < m; q++)    //循环模式sp
	{
		while (k > 0 && sp[k] != sp[q]) //不相等,k向后减,直到字符匹配或k = 0
			k = p[k-1];
		if (sp[k] == sp[q])       //如果相等k后移
			k++;
		p[q] = k;
	}
	cout << "计算出来的前缀数组是:" << endl;
	for (int i = 0; i < m; i++)
		cout << p[i] << " ";
	cout << endl;
}

//KMP算法
void KMPMatcher(string st, string sp)
{
	int n = st.length();
	int m = sp.length();

	int *p = new int[m];
	computePrefixFunction(sp, p);

	int q = 0;      //匹配的字符数
	for (int i = 0; i < n; i++)     //从左到右扫描st
	{
		while (q > 0 && sp[q] != st[i])   //当当前字符串不匹配,迭代求解q的值
			q = p[q-1];                   //即下一个p中与s[i]匹配的位置
	    if (sp[q] == st[i])    //如果当前字符匹配,则q后移
			q++;
		if (q == m-1)
		{
			cout << "Pattern occurs with shift " << i-m+1 << endl;
			q = p[q];
		}
	}
	delete [] p;
}
算法对st只扫描一遍,当不匹配时,从数组p中取出sp中下一个应该与st中不匹配字符比较的索引,所以运行时间为O(n),预处理时间为O(m)


你可能感兴趣的:(算法导论第32章 字符串匹配)