【算法分析whz】7.字符串搜索

目录

字符串匹配问题:

简单匹配算法的分析:

7.2 Rabin-Karp算法

基于指纹的算法 :

 使用Hash函数

预处理:求fingerprint

步骤:关键在每次移位的计算

Rabin-Karp算法:

分析:

应用:

 KMP算法

 自动机搜索

前缀函数:

 前缀表:

预先计算大小为m的前缀表来存储π[q]的值(0<=q

 KMP分析

BMH算法: 

逆简单算法:

启发式方法:

偏移表:

伪代码:

算法实现:

BMH分析:


字符串匹配问题:

输入:

文本T = "at the thought of"

n=length(T) =17

模式 P="the"

m=length(P) = 3

输出:移动到s (S是文本里的下标)

s是满足T[s....s+m-1] = P[0..m-1] 的最小整数(0<= s<=n-m)

如果不存在这样的s 返回-1

简单匹配算法: 

暴力搜索:        检查从 0 到n-m的所有值

伪代码

Native-Search (T,P)
for s <- 0 to n-m
	j <- 0
	//check if  T[s..s+m-1] = P[0..m-1]
	while T[s+j]= P[j] do
		j <- j+1
		if j = m return s
return -1 

令 T="at the thought of" , P="though"需要多少次比较?

简单匹配算法的分析:

最坏情况:

  • 外层循环: n-m 【P的第一个字母和T的字母对齐多少次】
  • 内层循环:m 【】
  • 总计(n-m)*m = O(nm)
  • 何种输入产生最坏情况?【每次P都是匹配到最后一个才发现不匹配】

最好情况:n-m 【每次一开始就发现不匹配】

完全随机的文本和模式:O(n-m)

思路: 如果文本完全随机,字母表大小是k,那么随便两个字母 每个字母是1/k 。P和T的第一个字母比较,他们匹配的概率是1/k ,不匹配的概率是1-1/k 。恰好比较两次的概率是(1/k)*(1-1/k)

7.2 Rabin-Karp算法

指纹想法

假设:

  • 我们可以在O(m)时间内计算一个P的指纹f(P)
  • 如果指纹不相等 f(P) ≠ f( T[s..s+m-1] ),那么子串不相等 P ≠  T[s..s+m-1] 
  • 我们可以在O(1)时间比较指纹
  • 我们可以在O(1)时间 从 f( T[s..s+m-1] ) 计算 f ' ( T[s+1..s+m] )  当前指纹和挪一位之后的指纹

基于指纹的算法 :

令字母表为∑ = { 0,1,2,3,4,5,6,7,8,9 }

令指纹为一个十进制数,即 f("1045") =1*10**3 + 0*10**2 +4*10**1 + 5 =1045

Fingerprint-Search (T,P)
	fp <- compute f(P)  //模式 或者说基准子串
	f <- compute f(T[0..m-1])

	for s <- 0 to n-m do
		if fp = f return s
		f <- (f - T[s]*10**(m-1) )*10 + T[s+m]
	
	return -1

运行时间是 2 O(m) + O( n-m) = O(n) 

开始算两个指纹 2 O(m) + 依次比较的时间 n-m轮 每轮是O(1) 

 使用Hash函数

问题: 实际上我们并不能假设我们可以对m位数在O(1)时间内进行算术运算。

解决方案:使用hash函数 h = f mod q 

例如,如果q=7 , h("52 ") = 52 mod 7 =3 

h(S1) ≠ h(S2) => S1 ≠ S2

但是h(S1) = h(S2) 不意味着 S1 = S2 

例如 ,如果q=7 ,h("73")=3 但 “73”≠“52”

但mod算数运算——操作数限定在q以内,如果q足够小/常数,就可以在O(1)

时间内 进行算术运算

(a+b)mod q= (a mod q +b mod q)mod q

(a*b)mod q=(a mod q)*(b mod q)mod q 

预处理:求fingerprint

拆开(秦九韶) 

fp=P[m-1] + 10* ( P[m-2] + 10*(P[m-3] +...+10*(P[1] + 10* (P[0]) ) )   mod q

同样可以从 T[0...m-1]计算ft

例如 P="2531" , q=7 ,fp =?

步骤:关键在每次移位的计算

ft =  ( ft-T[s]*10**(m-1) mod q) *10 + T[s+m] ) mod q

// T[s+m] 小于q 不需要mod q 。式子T[s]*10**(m-1)可能大于q 需要mod q ,而T[s]也是小于q ,所以10**(m-1)需要mod q

10**(m-1) mod q在预处理中计算一次

Rabin-Karp算法:

Karbin-Karp-Search (T,P)
	q <- a prime larger than m//选择一个大于m的素数q ,用来 
	c <- 10**(m-1) mod q //run a loop multiplying by 10 mod q
	fp <- 0 ; ft <- 0
	for i <- 0 to m-1 //preprocessing
	 	fp <- (10*dp +P[i] ) mod q
	 	ft <- (10*ft +T[i] ) mod q
	for s <- 0 to n-m //matching匹配
		if fp= ft then //指纹相等不代表字符串匹配
			if P[0..m-1] = T[s..s+m-1] return s//还需要匹配每一位字符
		ft <- (  (ft - T[s]*c ) *10 + T[s+m ] ) mod q //移位 
	return -1  

比较多少次?

分析:

如果q是素数,hash函数会使m位字符串在q个值中均匀分布。

        因此,仅有s个轮换中的每q次才需要匹配指纹(匹配慢,需要比较O(m)次 )。每q次需要一次指纹,其他需要一次比较。

期望运行时间(如果q>m)

        预处理:O(m) //需要对每个Pattern字符算一次

        外循环:O(n-m) //从头比到尾

        所有内循环:(n-m)/q * m = O(n-m) //具体指纹比较

        总时间:O(n-m) //线性

最坏运行时间:O(nm)  //和暴力搜索,每次都匹配上,但每次匹配到最后一个字母发现不一样

应用:

 KMP算法

N次比较的匹配

目标:文本中的每个字符仅匹配一次

简单算法的问题:

        没有利用已有部分匹配中的知识

        T="Tweedledee and Tweedledum"  P="Tweedledum"

        T="pappar"  P="pappappappar"

 自动机搜索

算法:

预处理:

        对于每个q (1 <= q <= m-1 ) 和每个α ∈ ∑ 预先计算一个q的新值,记为σ(q,α)

 填一个大小为 m* |∑| 的表

扫描文本:

当不匹配发现时,(P[q] ≠ T[s+q] ):置 s=s+q - σ(q,α)+ 1 ,且 q= σ(q,α)

 分析:

匹配阶段 O(n)

缺点:内存过多,O(m|∑ | ),过多的预处理 O(m|∑ | )

前缀函数:

 前缀表:

预先计算大小为m的前缀表来存储π[q]的值(0<=q<m  , m= length(P))

 

KMP-Search(T,P)
{
	π<- Compute-Prefix-Table(P)
	q <- 0 //匹配字符的数目 
	for i <- 0 to n-1 //从左到右扫描文本 
		while q >0 and P[q] ≠T[i] do
			q <- π[q] 
		if P[q] = T[i] then q <- q+1 
		if q= m then return i-m+1
	return -1
} 
//Compute-Prefix-Table 是P上执行KMP算法的本质 

 KMP分析

 最坏情况运行时间:O(n+m)

  •  主算法:O(n)
  • Compute-Prefix-Table : O(m)

空间:O( m ) 

主算法:最多匹配两次

BMH算法: 

逆简单算法:

从P的后面开始搜索。

Boyer and Moore

Reverse-Naive-Search (T,P)
	for s <- 0 to n-m
		j <- m-1 //从结尾开始
		//check if T[s..s+m-1] = P[0..m-1]
		while T[s+j] = P[j] do
			j <- j-1
			if j< 0 return s
	return -1 

运行时间和简单算法相同。

启发式方法:

 匹配P和T,从后往前匹配,

et a和e发现了不匹配,这个时候,只看最后字母e ,能和P里面的哪一个匹配(除了最后一个e 这位比较过了),

没有 ,这时候可以把P串到e的后面,相当于吧“date”串到cative这个地方,

再从后往前比,e和v不一样,把T[s+n-1]就是这个v 匹配到模式当中,去掉最后一位,比较过的这位以外,最右出现。我们知道前面没有v , 就可以把date再往后串4位 ,

这时候就把d对齐到v后边这个e的位置,然后,发现e和a不匹配,

再到P的前缀中找a , 发现有一个a ,就把这两个a 的位置对齐,然后e t a d 就找到了。

偏移表:

如果最后这个字母出现在P的前缀中,算P往后移动多少位才能跟他对齐,如果不出现,就跳过。

在预处理中,计算大小为|∑| 的偏移表。

 例:P="kettle " 去掉最后一个字母的前缀 "kettl" 如果最后一个字母是"e" shift[e] = 4 ,shift[l]=1,shift[t]=2,shift[k]=5 

伪代码:

BMH-Search (T,P)
    n <- the length of T
    m <- the length of P
    |∑| <- the length of OffsetTable

	//计算P的偏移表 
	for c <- 0 to |∑| -1 
		shift[c] =m //默认值

	for k <- 0 to m-2
		shift[P[k]] = m-1 -k 

	//查search
	s <- 0
	while s <= n - m do 
		j <- m-1 //从结尾开始
        // check if T[s..s+m-1] = P[0..m-1]
		while T[s+j] = P[j] do
			j <- j-1 
			if j < 0 return s
		s <- s + shift[ T[s+m-1] ] //shift by last letter
	return -1 

算法实现:

#include<bits/stdc++.h>
using namespace std;

const int maxnum=1005;
int shift[maxnum];

int BMH(string &T,string &P)
{
	int n=T.length();
	int m=P.length();
	
	//偏移表
	for(int i=0;i<maxnum;i++)
	{
		shift[i]=m;//默认值 
	}
	//模式串P中每个字母出现的最后下标,(除最后一个字母)
	//主串从 不匹配最后一个字符,需要左移的位数 
	for(int i=0;i<=m-2;i++)
	{
		shift[P[i]]=m-1-i;//字符-数值 
	}
	
	int s=0;// 模式串开始位置在主串的哪里
	
	while(s <= n-m)
	{
		int j=m-1;// 从模式串尾部开始匹配
		while(T[s+j]==P[j])
		{
			j=j-1;
			if(j<0)return s;// 匹配成功
		} 
		// 找到坏字符(当前跟模式串匹配的最后一个字符)
        // 在模式串中出现最后的位置(最后一位除外)
        // 所需要从模式串末尾移动到该位置的步数
		s=s+shift[ T[s+m-1] ];
	}
	return -1;
}

int main()
{
	string T,P;
	while(true)
	{
		getline (cin,T);
		getline(cin,P);
		//
		int res=BMH(T,P);
		if(res==-1 )
			cout<<"不匹配"<<endl;
		else 
			cout<<"模式串P在主串的位置为:"<<res<<endl;
	}
	return 0;
} 

测试

at the thought of
though
模式串P在主串的位置为:7
aaaaaaab
aaa
模式串P在主串的位置为:0

BMH分析:

 最坏情况运行时间:

  •         预处理:O( |∑| +m )
  •         搜索:O(nm) ---
  •         总计:O(nm)

空间:O( |∑|  )

  •         和m独立

在真实数据集合上很快。

这里提供基于C语言的单片机收发数据程序,使用的是STC89C52单片机,串口通信速率为4800bps。 ``` #include <reg52.h> //包含51单片机寄存器定义头文件 #define FOSC 11059200UL //晶振频率 #define BAUD 4800UL //波特率 #define TIMER_PRESCALER 12 bit flag; //标志位,标记是否接收到数据 unsigned char count = 0; //计数器,记录接收到的'A'的数量 void init_uart() //初始化串口 { TMOD |= 0x20; //设置定时器1为模式2,8位自动重装载 TH1 = 256 - FOSC/(TIMER_PRESCALER*BAUD*16); //设置波特率 TL1 = TH1; //赋初值 TR1 = 1; //启动定时器1 SCON = 0x50; //设置串口为模式1,允许接收 ES = 1; //开启串口中断 EA = 1; //开启总中断 } void uart_send(unsigned char dat) //发送数据 { SBUF = dat; //将数据存入发送缓冲区 while(!TI); //等待发送完成 TI = 0; //清除发送完成标志位 } void main() { init_uart(); //初始化串口 while(1) { if(flag) //如果接收到数据 { flag = 0; //清除标志位 if(SBUF == 'A') //如果接收到的是'A' { count++; //计数器加1 if(count == 10) //如果已经接收到10个'A' { uart_send('T'); //发送字符串TestOK uart_send('e'); uart_send('s'); uart_send('t'); uart_send('O'); uart_send('K'); uart_send('\n'); } else if(count == 20) //如果已经接收到20个'A' { uart_send('U'); //发送字符串UartOK uart_send('a'); uart_send('r'); uart_send('t'); uart_send(' '); uart_send('O'); uart_send('K'); uart_send('!'); uart_send('\n'); count = 0; //计数器清零 } } } } } void uart_isr() interrupt 4 //串口中断服务函数 { if(RI) //如果接收到数据 { RI = 0; //清除接收中断标志位 flag = 1; //设置标志位,表示接收到数据 } } ``` 这段代码实现了单片机与PC机进行串口通信,PC机发送字符给单片机,单片机统计收到的'A'宇符的个数,在数码管上显示个数。当收到10个'A'时发送宇符串TestOK。当收到20个 'A'时发送字符串“Uart oK!”。 需要注意的是,由于单片机与PC机进行的是异步串口通信,因此需要在程序中设置波特率、数据位、停止位、校验位等参数,以确保数据能够正确地传输。同时,要注意处理接收到的数据,避免出现数据丢失或错误的情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值