邓俊辉《数据结构》学习笔记-第十一章 串(自用)
1.基础知识
1.1 术语
1.2 ADT
2.串匹配(一系列字符构成的串n和子串m)
2.1 概念
文本串T,模式串P
四个层次问题:
算法评测:
2.2 算法
2.2.1 BF蛮力算法(O(n*m))
(1)i 和 j 分别指向T[ ]和P[ ]中待比对的字符
#include <iostream>
#include <cstring>
using namespace std;
int match(char *P,char *T){
size_t n=strlen(T),i=0;//[Error] 'strlen' was not declared in this scope
size_t m=strlen(P),j=0;//只需要 #include <cstring>
while(j<m&&i<n)
if(T[i]==P[j]){
i++;j++;
}
else{
i-=j-1;j=0;// T回退,P复位
}
return i-j;//返回匹配位置
}
int main(){
char *P="ll";
char *T="Hello";
cout<<match(P,T);
return 0;
}
(2)i+j 和 j 分别指向T[ ]和P[ ]中待比对的字符
int match2(char *P,char *T){
size_t n=strlen(T),i=0;//T[i]与P[0]对齐
size_t m=strlen(P),j;//T[i+j]与P[j]对齐
for(i=0;i<n-m+1;i++){//T从第i个字符起,与
for(j=0;j<m;j++){//P中对应的字符逐个比对
if(T[i+j]!=P[j]) break;//若失配,p整体右移一个字符重新比对
}
if(m<=j) break;//找到匹配子串
}
return i;//返回匹配位置
}
2.2.2 Knuth-Morris-Pratt :KMP算法(O(n))——记忆力与预知力
(1)算法——O(n+m)
int match_lookupTable(char *P,char *T){
int *next=buildNext(P);//构建查询表 ,O(m)
int n=(int)strlen(T),i=0;//
int m=(int)strlen(P),j=0;//
while(j<m&&i<n)//O(2n-1)
if(0>j||T[i]==P[j]){//j<0即对应比对在首字符失败
i++;j++;
}
else{
j=next[j];// P右移,T不回退,
}
delete [] next;
return i-j;//返回匹配位置
}
(2)查询表(next[ ]表)
1.含义及原理(经验)
2.构造(next[j]<j)
int *buildNext(char *P){
size_t m=strlen(P),j=0;
int *N=new int[m]; //next表
int t=N[0]=-1;//哨兵
while(j<m-1)
if(0>t||P[j]==P[t])
N[++j]=++t;
else
t=N[t];
return N;
}
3.改进(经验+教训)
int *buildNext(char *P){//改进
size_t m=strlen(P),j=0;
int *N=new int[m]; //next表
int t=N[0]=-1;//哨兵
while(j<m-1)
if(0>t||P[j]==P[t]){
j++;t++;
N[j]=P[j]!=P[t]?t:N[t];//若替代字符与被替代字符相同,则用N[t] 给N[j]赋值
}else
t=N[t];
return N;
}
4.与蛮力算法的对比
2.2.3 Boyer-Moore:BM算法——适用于字母表规模大的,如ASCLL,Unicode
结合了bc和gs表,最终的位移量选取其中的较大者
(1)BM_BC坏字符策略(最好O(n/m),最坏O(n*m))——重视教训
1.从后向前,自右向左的比对,匹配失败的情况出现的越早,排除的对齐位置越多(重视教训,因为失败的概率大于成功的概率)
概述:
2.构造bc[ ]表
int *buildBC(char *P){
int *bc=new int[256];//bc[]表,与字母表等长
for(size_t j=0;j<256;j++) bc[j]=-1;//初始化,统一指向通配符
for(size_t m=strlen(P),j=0;j<m;j++)
bc[P[j]]=j;//painter,保存的是最后出现的字母
return bc;
}//第二个循环,引入m来避免反复调用strlen
(2)BM_GS好后缀策略(经验:匹配的后缀)
1.概述
2.构造gs[ ]表(MS[ ] ->ss[ ]->gs[ ])
[1] MS[ j ]:P[ 0, j ]的所有后缀中,与P的某一后缀匹配最长者
[2] ss[ j ]=| MS[ j ] | = max { 0 <= s <= j+1 | P( j-s , j ] = P[ m - s , m ) }
int* buildSS ( char* P ) { //构造最大匹配后缀长度表:O(m)
int m = strlen ( P ); int* ss = new int[m]; //Suffix Size表
ss[m - 1] = m; //对最后一个字符而言,与之匹配的最长后缀就是整个P串
// 以下,从倒数第二个字符起自右向左扫描P,依次计算出ss[]其余各项
for ( int lo = m - 1, hi = m - 1, j = lo - 1; j >= 0; j -- )
if ( ( lo < j ) && ( ss[m - hi + j - 1] < j - lo ) ) //情况一:MS[ j ]足够长,以至于其就是整个模式串的前缀
ss[j] = ss[m - hi + j - 1]; //直接利用此前已计算出的ss[]
else { //情况二
hi = j; lo = __min ( lo, hi );
while ( ( 0 <= lo ) && ( P[lo] == P[m - hi + lo - 1] ) ) //二重循环?
lo--; //逐个对比处于(lo, hi]前端的字符
ss[j] = hi - lo;
}
return ss;
}
[3] gs[ j ]
int* buildGS ( char* P ) { //构造好后缀位移量表:O(m)
int* ss = buildSS ( P ); //Suffix Size table
size_t m = strlen ( P ); int* gs = new int[m]; //Good Suffix shift table
for ( size_t j = 0; j < m; j ++ ) gs[j] = m; //初始化
for ( size_t i = 0, j = m - 1; j < UINT_MAX; j -- ) //逆向逐一扫描各字符P[j]
if ( j + 1 == ss[j] ) //若P[0, j] = P[m - j - 1, m),则
while ( i < m - j - 1 ) //对于P[m - j - 1]左侧的每个字符P[i]而言(二重循环?)
gs[i++] = m - j - 1; //m - j - 1都是gs[i]的一种选择
for ( size_t j = 0; j < m - 1; j ++ ) //画家算法:正向扫描P[]各字符,gs[j]不断递减,直至最小
gs[m - ss[j] - 1] = m - j - 1; //m - j - 1必是其gs[m - ss[j] - 1]值的一种选择
delete [] ss; return gs;
}
2.3.4 总览
3.另类的串匹配:Karp-Rabin算法:散列(串即是数)
3.1 转化为数
3.2 散列
将每个串所对应的自然数称为其指纹,如果字符集规模很大或模式串P较长,则指纹将很大,导致数位溢出
此时就可以使用散列
(1)散列压缩
(2)散列冲突
遗留问题:快速指纹计算