理解串的模式匹配算法
串的模式匹配算法
相关约定
串
s
的0号位置存放的是串s
实际存放字符串的长度,如char* s=" abcd", s[0] = strlen(s)-1
主串记为
S
,模式串记为T
,匹配过程中主串索引为i
,模式串索引为j
匹配目标是在
S
中找到T
第一次出现的位置
朴素匹配算法
每次匹配失败后,主串的索引i
回溯到开头的下一个位置重新开始匹配
这次匹配的开头位置是i - (j - 1)
,下次从i - (j - 1) + 1
开始匹配
1 | int Index(const char* s, const char* t, int pos = 1){ |
KMP匹配算法
匹配失效的时候不回退i
,而是利用已经匹配的结果,尽量向右滑动模式串,对应j
向左移动到的下一个位置即为next[j]
,假设已经求出来了这个数组,KMP算法如下:
1 | int IndexKMP(const char* s, const char* t, int pos = 1){ |
求next数组
KMP算法的关键就在于如何求出next
数组
手算
当索引j
匹配失败时,说明前1~j-1
都是匹配成功了的,比如:
1 | i |
这里A B c d d A B
都已经成功匹配,并且注意到匹配成功的这部分的前后都有个A B
,于是可以将模式串向右滑动为:
1 | i |
这样滑动之后A B
仍然是匹配成功的,所以可以直接跳过,然后从A B
后面开始比较。为了知道能够把模式串滑动多远,就需要提前对模式串T
进行预计算,找到各种已经匹配长度对应最长相同的前后缀,滑动模式串就是移动j
到前缀的后一个位置。于是有下面的定义:
非常重要的几点:
- 这里的前缀和后缀是对
j
前面的字符串而言的(不包含j
本身对应的字符) - 要求一个字符串的前缀和后缀长度小于字符串本身(不然所有字符串的最长前后缀就都是自己了)
- 规定
next[1] = 0
而且必有next[2] = 1
,因为这里数组下标从1开始,下标2前面只有一个字符,所以前后缀长度只能是0。next
数组的前两个元素就是固定的0和1
下面是代码实现。
代码实现
代码实现里用到了类似于动态规划的思想,当求next[i]
时,由于next[i-1]
已经算好了,可以知道模式串1~(i-1)-1
的最长相同前后缀长度是next[i-1]-1
,也就是说next[i-1]
指向上一个前缀的后一个字符(刚好是下一步要比较的),比如:
1 | T: a b c d ... a b c d e |
已知next[i-1]=4
,现在求next[i]
。i指向的是可能的新后缀的下一个字符(要比较的是i-1的),比较T[next[i-1]]
和T[i-1]
是否相等,发现相等,最长相同前后缀就延长了,next[i]=next[i-1]+1
。
如果不相等呢?这个前缀长度不仅维持不了,而且可能一把回到解放前。比如:
1 | T: a ... b O ... a ... b X |
i-1指向X,而next[i-1]是O,原本的最长前后缀a...b
就无法延长了,而且因为X的格格不入,前后缀甚至可能没了。但是前后缀真的就没了吗?我们可以看看a...b
的细节,如果a...b
其实是a...b...a...b
的话,也就是说这个前缀自己也有个前缀:
1 | T: a ... b X ... a ... b O ... a ... b X ... a ... b X |
图片应该看得更明白:
上图中相同的矩形对应相同的字符串。当X不能延长上一个最长前后缀,那就尝试延长上一个最长相同前后缀的最长相同前后缀(有点套娃了,就先记作str吧),也就是a...b...a...b
里的a...b
。我们发现前缀的str后面有个X(下标为next[next[i-1]]),而后缀str正好可以跟i-1指向的X组合起来,从而1~i-1
的最长相同前后缀长度为next[next[i-1]]
,于是next[i]=next[next[i-1]]+1
。
这其实就是一种递归的思想,但是可以用动态规划代替,只需要一个变量j就可以了(类似于求斐波那契额数列的做法)。当发生不匹配时,让j“递归”去找前缀的前缀,直到匹配或者没有前缀为止。具体实现代码如下:
1 | void GetNext(const char* t, int next[]) { |
这里我故意把语句块都拆分开,条件分开判断,就是为了看得更清楚明白。数据结构书上的代码是这样的:
1 | void GetNext(const char* t, int next[]){ |
简洁是挺简洁的,而且形式上与KMP查找时很像,但是感觉有点为了写代码而写代码了,不好理解:(
示例
j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
T | a | a | a | a | b |
next[j] | 0 | 1 | 2 | 3 | 4 |
next数组改进
按照上面求出的next
数组,当模式串j
处发生失配时,j
需要改变为next[j]
。但是如果next[j]
指向的与j
指向的字符相同,则之后匹配一定还是失败的。这时只能利用更短一些的前缀,把j
设成next[next[j]]
。这样改进后的数组命名为nextval
。
手算
先算出next
数组,然后令nextval[1] = 0
,从左向右逐个看j
处的字符是否与next[j]
的相同,如果是,就将对应nextval
设为nextval[nextval[j]]
,否则还是原来的next[j]
。
代码
只需要在给next[i]
赋值之后加这一句:
1 | if(t[i] == t[next[i]]){ |
示例
j | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
T | a | a | a | a | b |
next[j] | 0 | 1 | 2 | 3 | 4 |
nextval[j] | 0 | 0 | 0 | 0 | 4 |
使用std::string的版本
主要是把下标改成从0开始的,next[0] = -1
。可以过这道题:实现 strStr()
1 | void GetNext(const string& t, vector<int>& next){ |
还有一种写法是按照书本里改的,虽然更简洁一些,但是我觉得容易理解的代码才容易记住,只是形式上好看的话以后还是会弄混。
1 | // 朴素匹配算法 |