王道章节内容
知识框架
考纲内容
字符串模式匹配
串的定义和实现
定义
抽象数据类型描述
串的逻辑结构与线性表类似,不同之处在于串针对的是字符集
顺序存储结构
定义
串的顺序存储结构
- 用一组地址连续的存储单元来存储串中的字符序列;
- 按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区;
- 一般用定长数组来定义。
定长数组的基本特性:
- 固定大小:数组的大小(即它可以容纳的元素数量)是在创建时定义的,并且在程序运行期间保持不变。
- 连续存储:数组中的元素在内存中是连续存储的,这使得随机访问非常快。
- 索引访问:可以通过索引来直接访问数组中的任何元素。索引通常从0开始。
- 类型化:数组中的所有元素都具有相同的类型。
定长顺序存储表示
堆分配存储表示
链式存储结构
块链存储结构
串的模式匹配
定义
串的模式匹配
子串的定位操作,例如一个英文单词在一篇英文文章的定位。
朴素的模式匹配算法
算法思路
- 简单说,就是对主串 S 的每一个字符作为子串 T 开头,与要匹配的字符串进行匹配;
- 对主串 S 做大循环,每个字符开头做子串 T 的长度的小循环,指导匹配成功或者全部遍历完成。
/* 朴素的模式匹配法 */
int Index(String S, String T, int pos)
{
int i = pos; /* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */
int j = 1; /* j用于子串T中当前位置下标值 */
while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */
{
if (S[i] == T[j]) /* 两字母相等则继续 */
{
++i;
++j;
}
else /* 指针后退重新开始匹配 */
{
i = i-j+2; /* i退回到上次匹配首位的下一位 */
j = 1; /* j退回到子串T的首位 */
}
}
if (j > T[0])
return i-T[0];
else
return 0;
}
代码解释
i = i - j + 2;
缺陷
也有教材说为O((n - m + 1)* n),显然哈哈。
KMP 模式匹配算法
KMP 模式匹配算法,即克努特-莫里斯-普拉特算法。
引入
从上面的朴素的模式匹配中,我们知道,有些过程是不必要的,这取决于前面的元素后边是否有所重合,例如子串 abcde,长度为5,我们可以注意到开头的 a 与之后的都不重合,而假设前3位匹配上了,那么从第2和第3位开始的匹配操作都是不必要的了,直接从第四位开始匹配即可;
又比如,abcefabde,可以看出,前缀 ab (1,2)与后缀 ab(7,8) 有所重合,假设前8位匹配上了,那么同样第7位之前的匹配操作都是不必要的了,直接从第7位开始即可;
这里需要注意,我举的例子都是“匹配数(即最大 i 值)>=相同后缀首字符位置数”的,如果是 < 的情况,那自然要回溯到 i - j + 2 位置开始;
如果利用这一特性去减少主串 i 的回溯,即不让 i 变小,那么我们只要考虑子串 j 的变化即可,即 j下一次从哪里开始,又因为后缀重合部分是已经确定的、毋庸置疑的,所以 j 的变化显然就与“当前字符之前的串的前后缀之间的相似度”有关;
怎样更好地理解以上这些呢,我们可以把 “主串”类比为一个“漆黑的电影院里的一排座位”,“子串”类比为“一摞子顺序固定的票”,手里的票的数目是已知的,票号和电影院里的座位号都是未知的。每一次匹配都是一次“开灯”,我们只能通过“开灯”得到的信息进行进一步的操作。
电影院座位与票的对应:
- 每个座位代表主串中的一个字符位置。
- 每张票代表子串中的一个字符。
开灯:
- 开灯意味着我们正在比较主串中的一个字符与子串中的一个字符。
- 当我们开灯时,我们能看到一个座位与一张票之间的对应关系。
- 如果座位号与票号匹配,那么我们向前移动到下一个座位和下一张票。
- 如果不匹配,我们需要考虑如何重新开始匹配。
优化匹配过程:
- 当我们遇到不匹配的情况时,我们需要决定如何继续匹配。
- 如果子串中有前后缀重合的部分,那么我们可以跳过一些不必要的座位,直接从一个合理的座位开始比较。
- 例如,如果我们已经比较了子串的一部分,而这一部分与子串的末尾有重合,那么我们可以直接跳到重合部分的末尾开始比较。
推导
《大话》和王道针对 j 的变化(即相似度)分别给出了两种表示方法,这两种方法是殊途同归的。
王道表示:
到这里实际上与《大话》一样了哈哈哈哈哈。
《大话》表示:
这里看的时候可以用“左闭右开”区间来理解,如 i:[1,i);
根据经验可知,最开始 k = 0;接着第二个若没有相等,则 k = 1;接着还没有则回溯;如果前后缀一个字符相等,k = 2;如果前后缀 n 个字符相等,则 k = n + 1 。
综上,next [ j ] 的意思是:当子串的第 j 个字符与主串发生失配时,跳到子串的 next [ j ] 位置进行比较。
代码实现
王道与《大话》实现基本类似,故不特别标明。
求要匹配的子串的 next 数组:
/* 通过计算返回子串T的next数组。 */
void get_next(String T, int *next)
{
int i,k;
i=1;
k=0;
next[1]=0;
while (i<T[0]) /* 此处T[0]表示串T的长度 */
{
if(k==0 || T[i]== T[k]) /*这里指相似
{
++i;
++k;
next[i] = k; /*
}
else
k= next[k]; /* 若字符不相同,则k值回溯上一个 */
}
}
这里的代码可能不好理解,特别是针对 k 和 i,这里我们作出以下解释:
变量说明:
i
: 当前正在处理的模式串T
中的位置索引。k
: 已经匹配的字符个数,或者说当前正在尝试匹配的模式串T
中的位置索引。
i
和k
的作用:
i
: 在每次循环中,i
指向模式串T
中当前要比较的字符。初始时,i
为 1,指向模式串的第一个字符。k
:k
指向模式串T
中正在尝试匹配的前缀的最后一个字符。初始时,k
为 0,表示还没有匹配任何符。k 值在循环中是动态的,有时作为值直接存储为next,有时作为中间状态值0进行下一步的迭代。(所以k值在循环中并不是处处符合数学式,但最后展现是符合预期的)
为什么呢?
因为 next[ j ] 和 k 数量意义上不是一直互通的,一个是成熟的,一个是初始的。
让我们用字符串 "abcac" 作为例子来具体解释 i
和 k
的变化:
匹配查找:
/* 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。 */
/* T非空,1≤pos≤StrLength(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;
}
对比朴素的模式匹配算法,去除了 i 的回溯,多了以下部分:
int next[255]; /* 定义一next数组 */
get_next(T, next); /* 对串T作分析,得到next数组 */
else /* 指针后退重新开始匹配 */
j = next[j];/* j退回合适的位置,i值不变 */
改进
/* 求模式串T的next函数修正值并存入数组nextval */
void get_nextval(String T, int *nextval)
{
int i,k;
i=1;
k=0;
nextval[1]=0;
while (i<T[0]) /* 此处T[0]表示串T的长度 */
{
if(k==0 || T[i]== T[k]) /* T[i]表示后缀的单个字符,T[k]表示前缀的单个字符 */
{
++i;
++k;
if (T[i]!=T[k]) /* 若当前字符与前缀字符不同 */
nextval[i] = k; /* 则当前的j为nextval在i位置的值 */
else
nextval[i] = nextval[k]; /* 如果与前缀字符相同,则将前缀字符的 */
/* nextval值赋值给nextval在i位置的值 */
}
else
k= nextval[k]; /* 若字符不相同,则k值回溯 */
}
}
解释:
nextval[k]
:
nextval[k]
表示在位置k
上的最长相同前后缀的长度,但如果前后缀相同,则向前追溯到下一个不同的字符。- 例如,如果
T
为 "ababa",那么nextval[5]
会是 2,而不是 4,因为 "ababa" 的最长相同前后缀是 "ab" 而不是 "abab"这里的操作相当于“先看看”到底前缀有几个连续相同,然后再“一起挪动”。
实现匹配算法,只需改动声明 “ get_next( T, next ); ” 为 “ get_nextval( T, next);” 即可。
理解
这里结合“电影院开灯找座位”和“英文文字找单词”的例子,找之前先入为主能找着“单词”,通过一次又一次的“开灯”所得到的有限信息,做出“移位操作”;
比如说,从一段“无限长度”的话(有头不知尾),从前往后,先找a开头的,再找ab开头的,不匹配时考虑往后是选择怎样的“前缀”继续找;
倘若出现连续的情况,比如单词为aaaac,那就找到好几个aaa开头的,当发现文章中从第几个开始不匹配时,自然而然整个挪到后边比较。
所以KMP就是 next + 匹配。
碎碎念
要怎样奔跑的努力,让所有的闲言碎语都追赶不及。。。