目录
1 串简介
串是由零个或多个字符组成的有限序列,又叫字符串。
计算机中字符串使用ASCII或者Unicode编码。对于两个串不相等时,如何判定它们的大小,我们这样定义:
给定两个串:s="a1a2...an",t="b1b2...bn",当满足以下条件之一时,s<t。
n<m,且ai=bi,例如s=“hap”,t="happy",就有s<t。
存在某个k<min(m,n),使得ai=bi,ak<bk,例如s=“happen”,t="happy",因为两串的前4个字母均相同,比较第5个字母的ascii码有s<t
串的逻辑结构和线性表很相似,不同之处在于串针对的是字符集,也就是串中的元素都是字符。
串的存储结构于线性表相同分为两种。
串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列,按照预定义的大小为每个定义的串变量分配一个固定长度的存储区,一般是用定长数组来定义。串值的存储空间可在程序执行过程中动态分配而得,比如在计算机汇总存在一个自由存储区,叫做“堆”。
串的链式存储结构,与线性表相似的,但由于串结构的特殊性,结构中每个元素数据是一个字符,如果也简单的应用链表存储串值,一个结点赌赢一个字符,就会存在很大的空间浪费,因此,一个结点可以存放一个字符,也可以考虑存放多个字符,最后一个结点若是未被占满时,可以用“#”或其他非串值字符补全。
2 模式匹配算法
2.1 朴素的模式匹配算法
假设主串s="goodgoogle",找到t="google"这个子串的位置:
主串s第一位开始,s与t前三个字母匹配成功,但s第4个字母是d而t的是g,匹配失败,然后子串后移一位与主串的o比较,不匹配,子串继续后移,继续后移直到子串匹配成功。如下图:
2.2 KMP模式匹配算法
主串s=“abcdefgab”,子串t="abcdex",如果用前面的朴素算法的话,前5个字母,两个串完全相等,直到第6个字母,“f”与"x"不等。
接下来,按照朴素模式匹配算法,即主串s中当i=2,3,4,5,6时,首字符与子串t的首字符均不等。仔细观察,对于要匹配的子串t来说,“abcdex”首字母“a”与后面的串“bcdex”中任意一个字符都不相等,也就是说,既然“a”不与自己后面的子串中任何一字符相等,前5位字符分布相等,意味着子串t的首字符"a"不可能与s串的第2位到第5位的字符相等,如上图2,3,4,5的判断都是多余的,从上分析可知子串需要回溯到主串位置,那么回溯的位置是多少?
先看一个前缀和后缀的例子:abc,前缀a,ab;后缀c,bc
例如子串"abcabx",next数组求解步骤:
1,列出表达式
a
a b
a b c
a b c a
a b c a b
a b c a b x
2,计算每行前缀和后缀重合数
例如第一行为0;
第二行前缀a,后缀b ,最大相等子串长度为0;
第三行前缀a,ab,后缀c,bc,最大相等子串长度为0
第四行前缀a,ab,abc,后缀a,ca,bca,最大相等子串长度为1
第五行前缀a,ab,abc,abca,后缀b,ab,cab,bcab,最大相等子串长度为2
第六行前缀a,ab,abc,abca,abcab,后缀x,bx,abx,cabx,bcabx,最大相等子串长度为0
得到maxL串:0 0 0 1 2 0
3,计算next值
去掉maxL串中最后一位,在最前面一位加上-1,得到-1 0 0 0 1 2,在每个上加1得到最后的next值0 1 1 1 2 3
代码如下:
/*通过计算返回子串t的next数组*/
void get_next(String t, int *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;
++j;
next[i] = j;
} else {
j = next[j]; /*若字符不相同,则j值回溯*/
}
}
}
/*返回子串t在主串s中第pos个字符之后的位置,若不存在,则函数返回值为0*/
/*t非空,1<=pos<=stringLength(s)*/
int index_kmp(String s,String t, int pos) {
int i = pos; /*i用于主串s当前位置下标值,若pos不为1,则从pos位置开始匹配*/
int j = 1; /*j用于子串t中当前位置下标值*/
int next[255]; /*定义一next数组*/
get_next(t, next); /*对串t作分析,得到next数组*/
while(i<= s[0] && j<=t[0]) { /*若i小于s的长度且j小于t的长度时,循环继续*/
if(j==0 || s[i]==t[j]) {/*两字母相等则继续,与朴素算法增加两j=0判断*/
++i;
++j;
} else { /*指针后退重新开始匹配*/
j = next[j]; /*j退回合适的位置,i值不变*/
}
}
if(j>t[0]) {
return i-t[0];
} else {
return 0;
}
}
2.3 KMP模式匹配改进算法
由上我们知道子串需要回溯到主串的位置,那么同理子串内也是可以回溯的,回溯的位置基于next数组计算得到:
j | 1 | 2 | 3 | 4 | 5 | 6 |
a | b | c | a | b | x | |
maxL | 0 | 0 | 0 | 1 | 2 | 0 |
next | 0 | 1 | 1 | 1 | 2 | 3 |
nextVal |
按次序检查maxL和next是否相等,若不等则填入到nextval中;若相等填入对应序号的next到nextval。例如其中编号为4的a,它的maxL值与next值相等,所以nextval值等于编号为1的next值0,结果如下:
j | 1 | 2 | 3 | 4 | 5 | 6 |
a | b | c | a | b | x | |
maxL | 0 | 0 | 0 | 1 | 2 | 0 |
next | 0 | 1 | 1 | 1 | 2 | 3 |
nextVal | 0 | 1 | 1 | 0 | 1 | 3 |
KMP改进算法如下:
/*求模式串t的next函数修正值并存入数组nextval*/
void get_nextval(String t, int *nextval) {
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为nextval在i位置的值*/
} else {
nextval[i] = nextval[j]; /*如果与前缀字符相同,则将前缀字符的nextval值赋值给nextval在i位置的值*/
}
}else {
j = nextval[j]; /*若字符不相同,则j值回溯*/
}
}
}
参考视频:
https://www.bilibili.com/video/av22409335/?spm_id_from=333.788.videocard.3