串
由零个或者多个字符组成的有限序列,又名字符串
区分空格串和空字符串
ADT 串(string)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继关系
Operation
StrAssign(T,*chars); //生成一个值等于字符串常量chars的串T
StrCopy(T,S); //若串S存在,由串S复制得串T
ClearString(S); //若串S存在,将串清空
StringEmpty(S); //串S为空,返回true,否则返回false
StrLength(S); //返回串S的元素个数,即串的长度
StrCompare(S,T); //若S>T,返回值>0;若S=T,返回0,若S<T,返回<0
Concat(T,S1,S2); //用T返回S1,S2连接而成的新串T
SubString(Sub,S,pos,len); //串S存在,且1<= pose <=StrLength(S),0<=len<=StrLength(S)-pose+1
//用Sub返回串S的第pos个字符起长度为len的子串
Index(S,T,pos); //若S,T存在,T为非空,且1<= pose <=StrLength(S),若S中存在与T串的值相等的子串,则返回它在 //第pos个字符之后第一次出现的位置,否则返回0
Replace(S,T,V); //用V替换S中出现的T
StrInsert(S,pos,T); //S中第pos个字符后插入T
StrDelete(S,pos,len); //删除串S第pos个字符后的长度为len的子串
endADT
Index操作
int Index(String S,String T,int pos)
{
int n,m,i;
String sub;
if(pos>0)
{
n = StrLength(S); //主串长n
m = StrLength(T); //子串长m
i = pos;
while(i<n-m+1) //循环n-m+1-pos次
{
SubString(sub,S,i,m); //从pos开始取S后的m个数,赋给sub
if(StrCompare(sub,T) != 0)
i++; //不匹配继续后移
eles
return i; //匹配则返回该处位置i
}
}
return 0;
}
串的顺序存储结构
规定在最后加入”\0“来表示串值的终结,其占用一个空间,但是不算入串长度
方法一: 使用定长数组存储
方法二:使用malloc()\free() 或者new\delete 来实现动态分配
串的链式存储结构
链式结构相似于线性表,但是一个节点存储一个字符太过浪费空间,所以可以一个节点存储多个字符,若有未满的节点,可用”#“或其他非串值字符补全。
该结构在连接串时有一定优势,但是总的来说不如顺序存储灵活。
朴素模式匹配算法
子串的定位操作通常称为串的模式匹配,是串操作中最重要的操作之一。
朴素的匹配算法即如前面的代码一样,逐个遍历比较,以下为只是用基础数组来实现的方法。
假设S[0]与T[0]中存储了串的长度
int Index(String S,String T,int pos)
{
int i = pos; //记录起始位置
int j = i;
while(i<=S[0] && j<= T[0]) //i<小于S长度,且j小于T的长度
{
if(S[i] == T[j]) //若匹配则一直继续
{
++i;
++j;
}
else //若不匹配,则i相比pos之前+1,重新匹配
{
i = i-j+2;
j = 1; //j回退到T首位
}
}
if(j > T[0]) //如果j大于T的长度,即完全匹配
return i-T[0]; //返回在S中的位置
else
return 0; //若是超过S的长度,则返回0
}
缺点在于效率低,复杂度为O{(n-m+1)*m}
KMP模式匹配算法
当子串T中具有大量重复的字符,如abcdabcde时,可以使用该算法来简化匹配算法
将T串各个位置的j值的变化定义为一个数组next,其next的长度为T的长度
next[j]的值(也就是k)表示,当T[j] != P[i]时,j指针的下一步移动位置
j=1时 next[1] = 0;
Max{k|1<k<j,且'p1...pk-1' = 'pj-k+1...pj-1'} next[j] = Max
其他情况 next[j] = 1;
以下为一些例子
例1:
j | 123456 |
模式串T | abcdex |
next[j] | 011111 |
例2
j | 123456 |
模式串T | abcabx |
next[j] | 011123 |
j为5时 1到j-1的串为abca p1 = p4 个数为1 所以next[j] = 2
j为6时 1到j-1的串为abcab p1p2 = p4p5 个数为2 所以next[j] = 3
例3
j | 123456 |
模式串T | ababaaaba |
next[j] | 011234223 |
j为4时 串为aba next[j] = 2
j为5时 串为abab next[j] = 3
j为6时 串为ababa p1p2p3 = p3p4p5 所以next[j] = 4
j为7时 串为ababaa p1=p6 next[j] = 2
由上述的规则可以获得数组next的获得代码如下
void get_next(String T,int *next) //计算T的next数组
{
int i,j;
i = 1;
j = 0;
next[1] = 0; //初始化
while(i<T[0]) //此处T[0]为串T的大小
{
if(j == 0||T[i] == T[j] ) //T[i]代表后缀的单个字符 T[j]代表前缀的单个字符
{
++i; //i = 2
++j; //j = 1
next[i] = j; //next[2] = 1;
}
else
j =next[j]; //若字符不同,则j值回溯
}
}
ababc
i = 1 j = 0 next[1] =0;
i = 2 j = 1 next[2] =1;
j = next[1] = 0;
i = 3;j = 1; next[3] = 1;
T[3] = T[1]; i=4 j=2 next[4] = 2;
T[4] = T[2]; i=5 j=3 next[5] = 3;
T[5] != T[3]; j=next[3] = 1;
T[5] != T[1]; j=next[1] = 0;
i=6;j=1;next[6] = 1;
当T[i] = T[j]时,有next[j+1] == next[j] + 1
当T[i] != T[j]时,有j = next[j];
上述代码不太好理解,抽象地说来,就是初始化next[1]为0;然后当j为0或者有字符相等时,i与j均++,然后计算next的值即next[i] = j;而如果字符继续相等,则next的下一个值会继续在j的基础上加下去,而i则不断推进。一旦字符不相等了,j会随着next[j]的值进行回溯,直至相等或者回退到0,然后重新开始匹配。
有了next指针,kmp算法如下
int Index_KMP(String S,String T,int pos)
{
int i = pos; //记录起始位置
int j = 1; //修改1
int next[255];
get_next(T,next); //获得next数组
while(i<=S[0] && j<= T[0]) //i<小于S长度,且j小于T的长度
{
if(j==0||S[i] == T[j]) //修改2 若匹配或j回溯到0则一直继续
{
++i;
++j;
}
else //若不匹配,则i相比pos之前+1,重新匹配
{
j = next[j]; //修改3 j回退到T首位
}
}
if(j > T[0]) //如果j大于T的长度,即完全匹配
return i-T[0]; //返回在S中的位置
else
return 0; //若是超过S的长度,则返回0
}
其相比朴素匹配算法仅修改了3处并增加了next数组,相比而言优势在于其i值没有进行回溯。复杂度为O(n+m)
KMP模式匹配算法改进
缺陷:假设主串为aaaadef 模式串为aaaabcd则在i=5,j=5时,发现不相等 于是j值需要依次回溯从4直到1;最终在i=6,j=1时终结;可以发现这个过程是多余的。
改进:当前4个字符均相等时,可以使用首位的next[1]的值代替与他相等的字符后续的值,改进后的数组为nextval
例
j | 123456789 |
模式串T | ababaaaba |
next[j] | 011234223 |
nextval[j] | 010104210 |
j | 123456789 |
模式串T | aaaaaaaab |
next[j] | 012345678 |
nextval[j] | 000000008 |
计算过程即为判断第j个字符,与第next[j]个字符是否相等,相等则nextval[j] =nextval[next[j]] ,否则nextval[j] = next[j];
求nextval程序如下
void get_next(String T,int* nextval) //计算T的next数组
{
int i,j;
i = 1;
j = 0;
nextval[1] = 0; //初始化
while(i<T[0]) //此处T[0]为串T的大小
{
if(j == 0||T[i] == T[j] ) //T[i]代表后缀的单个字符 T[j]代表前缀的单个字符
{
++i;
++j;
if(T[i] != T[j]) //若与前缀字符不同
nextval[i] = j; //则为j值即原next[i]值
else
nextval[i] = nextval[j]; //若相等,则为nextval[j]值
}
else
j =nextval[j]; //若字符不同,则j值回溯
}
}
可见相比之前的KMP主要区别在于存next时进行了判断是否与之前的next指向的字符串相等。