1.暴力匹配方法
假设现在我们面临这样一个问题:有一个文本串S,和一个模式串P,现在要查找P在S中的位置,怎么查找呢?
如果用暴力匹配的思路,并假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置,则有:
- 如果当前字符匹配成功(即S[i] == P[j]),则i++,j++,继续匹配下一个字符;
- 如果失配(即S[i] ! = P[j]),令i = i - (j - 1),j = 0。相当于每次匹配失败时,i 回溯,j 被置为0。
代码如下:
int ViolentMatch(char* s, char* p)
{
int sLen = strlen(s);
int pLen = strlen(p);
int i = 0;
int j = 0;
while (i < sLen && j < pLen)
{
if (s[i] == p[j])
{
//①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++
i++;
j++;
}
else
{
//②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0
i = i - j + 1;
j = 0;
}
}
//匹配成功,返回模式串p在文本串s中的位置,否则返回-1
if (j == pLen)
return i - j;
else
return -1;
}
Q:为什么是 i - j -1?
A:因为主串的i和模式串的j此时是对齐匹配的,发现不相同,那么此时模式串和主串匹配的起点是i-j,往前递增一位,自然是i-j+1
2.KMP算法
KMP算法相比暴力法而言,有效利用了模式串自身的特性,从而提高了效率。
下面先直接给出KMP的算法流程:
假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置
-
如果 j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++,继续匹配下一个字符;
-
如果 j ! = -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]。此举意味着失配时,模式串P相对于文本串S向右移动了 j - next [j] 位。
换言之,当匹配失败时,模式串向右移动的位数为:失配字符所在位置 - 失配字符对应的next 值,即移动的实际位数为:j - next[j],且此值大于等于1。
很快,你会意识到next 数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] = k,代表 j 之前的字符串中有最大长度为k 的相同前缀后缀。
此也意味着在某个字符失配时,该字符对应的next 值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next [j] 的位置)。
-
如果next [j] 等于0或-1,则跳到模式串的开头字符
-
若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了k 个字符(跳过的都是模式串本身重复的)。
牢牢记住:KMP算法保证主串的i值不回溯,next[j]代表当发生匹配失败时,j下次应该指向哪里(指向next[j]),来再次和主串i(不回溯)进行匹配。
2.1 什么叫前缀后缀最长公共元素长度?
前缀:是指除了字符最后一个元素外,其余全部的子集
后缀:是指除了字符第一个元素外,其余全部的子集
做成表格形式为:
这就是模式串对应的最大长度表,j=0时,最大长度为0,j=1时,最大长度为0.....j=5时,最大长度为2,j=6时,最大长度为0;
每个j对应有一个:从第一个字符到j字符这段字符前后缀最大公共元素长度
2.2 什么是j对应的next数组?
当匹配到一个字符失配时,其实没必要考虑当前失配的字符(第j个),而是看失配字符的上一位字符(第j-1个)对应的公共元素最大长度值。如此,便引出了next 数组。
next[j]的含义:j对应的next数组就是其上一个元素即j-1个对应的最大公共元素长度
在KMP算法中的含义:next[j]代表当发生匹配失败时,j下次应该指向哪里(指向next[j]),来再次和主串i(不回溯)进行匹配。
next数组就干了两件事:
- 最大公共长度表右移一位
- 初始值为-1
如下图便是j对应的next数组
2.3 如何用代码求next数组?
我们已经知道的是:next[0] =-1;next[1] = 0;
我们要用代码求出next数组,其核心问题是:
怎么由next[j] -> next [j+1]?
假定给定模式串ABCDABCE,且已知next [j] = k,现要求next [j + 1]等于多少?
因为我们已经知道next[j] = k,就是说j前面的字符前缀和后缀有k个重复的,即相当于前缀->p0... pk-1(k个) = 后缀 -> pj-k... pj-1(k个)
这个只要知道 next[j] = k,上面就一定成立,这个条件我们要牢记。
已知next[j] =k ,即已知的是两个值:k和j,那我们先判断下pk 和 pj的值,
①pk = pj
若pk =pj,如图例, j = 6,此时pk = pj = C,所以:
next [j + 1 ] = next[ j ] + 1 = k + 1(可以看出next[j + 1] = 3)
代表字符E前的模式串中,有长度k+1 的相同前缀后缀。
其实很好理解:
因为我们知道了next[j],又next[k]和next[j]是对称的(从0~k-1(下标k前面的)有k个,j-k到j-1(下标j前面的)有k个,这k个是两者重复的元素)
若此时再:next[k] == next[j],即本来又对称,现在两边又各自多了一个相同的元素,当然也对称了
所以其前缀和后缀相同有k+1个,即next[j+1] =next[j] +1 = k + 1;
其实这也代表着元素E前面有多少个相同的前缀和后缀(next[j+1]的含义)
![](https://i-blog.csdnimg.cn/blog_migrate/053db69b43c0b8322bf7c86f9511c2de.png)
②pk != pj
若pk != pj,如图例,那么就说明:
存在: p0... pk-1 = pj-k... pj-1 (因为next[j]=k)
不存在:p0... pk-1 pk = pj-k... pj-1 pj(因为pk != pj)
很明显,因为C不同于D(pk != pj),所以ABC 跟 ABD不相同,即字符E前的模式串没有长度为k+1的相同前缀后缀,也就不能再简单的令:next[j + 1] = next[j] + 1 。所以,咱们只能去寻找长度更短一点的相同前缀后缀。
因为我们要求的是E前面有多少个同前缀和后缀,我们以后缀为中心,已经知道后缀pj=D,既然是找前缀==后缀最大长度,那就是找一个前缀有D:AXXX...D == 后缀AXXX...D
(根据定义就能知道,前缀必包含第一个字母,后缀必包含最后一个字母)
既然已经知道pk!=pj了,那当务之急是我们找到一个:等于pj的新的pk'(如果有,它必然是D)
结合上图来讲,若能在前缀“ p0 pk-1 pk ” 中不断的递归前缀索引k = next [k],找到一个字符pk’ 也为D,代表pk’ = pj,且满足p0 pk'-1 pk' = pj-k' pj-1 pj,则最大相同的前缀后缀长度为k' + 1,
从而:
next [j + 1] = k’ + 1 = next [k' ] + 1
否则:
前缀中没有D,则代表没有相同的前缀后缀
即:next [j + 1] = 0
如上图中青黄色就是找到的新的k',即k' = next[k]
Q:那为何k = next [k]呢?
理解:
注意看这个红黄蓝分区图,我们要求的是next[j+1],我们已经知道的是:
- next[j] = k //它代表在Pj前面,有k个同前缀和后缀
- Pk!=Pj
next[j+1] 即代表着j+1前面j个字符最大同前后缀长度,我们先假设:存在一块前缀区域从P0~Px,等于后缀区域Pj-x~Pj-1,即Px = Pj
要知道P0~Pk-1(k个前缀) 和Pj-k~Pj-1(k个后缀)可是完全相等的,他俩大范围都相等,那对应子区肯定相等!那么根据对称原理,已知下图区域1和4相同,可以发出三个灵魂质问:
区域1和区域3是不是完全相等? 当然是!
区域2和区域4是不是完全相等? 当然是!
那区域1和区域2是不是完全相等?当然是!
所以:如果我们再能找到存在这么一个Px,即我们要找到一块1区域和区域4相等,就是在k前面的字符找到一块1区域使2区域相等?
这不就是对下标Pk进行求最大前缀和后缀吗?
也就是求next[k]啊!
故:找到这么一个新的k',使得k' =next[k]
写成代码如下:
void GetNext(char* p,int next[])
{
int pLen = strlen(p);
next[0] = -1;
int k = -1;
int j = 0;
while (j < pLen - 1)
{
//p[k]表示前缀,p[j]表示后缀
if (k == -1 || p[j] == p[k])
{
++k;
++j;
next[j] = k; //即next[j+1]=next[j]+1=k+1;
}
else
{
k = next[k];
}
}
}
2.4 为什么i和j不匹配时,j要指向next[j]?
这其实是问next[j]有什么作用,为什么用于KMP算法能提高效率?
其实意味着在某个字符失配(Pj != Pi)时,j字符对应的next[j] 值会告诉你下一步匹配中,模式串下一步应该跳到哪个位置(跳到next [j] 的位置)。如果next [j] 等于0或-1,则跳到模式串的开头字符,若next [j] = k 且 k > 0,代表下次匹配跳到j 之前的某个字符,而不是跳到开头,且具体跳过了j-k 个字符。
如图:
此时 i = 9,j = 6时,发生不匹配的状况,由此我们知道:在j==6时,i和j才不匹配,那么0~j-1对应长度肯定是匹配的。
j-1有最长后缀和前缀串AB,既然已经知道i的ABCDAB和j的i的ABCDAB是匹配的,那么模式串的后缀串AB肯定也是匹配的,模式串的后缀串AB又等于模式串的前缀串AB,那肯定模式串的前缀AB也和主串后面的AB是匹配的!
直接从模式串前缀串AB后面的开始比较不就行了?这就是我们的next[j]的作用。
next[j] == 2那么从模式串[2]的元素开始和 i 匹配即可,即第三个元素开始和 i 匹配,也就是让模式串的第三个元素C和主串的空格对齐,此时 j = 2,i = 9 不变,也就是j指向了next[j],继续开始匹配。
2.5 模式串移动了多少?
让 j = next[j],看起来是让主串的后缀串和模式串的前缀串对齐了,但因为模式串的后缀串已经和主串的后缀串匹配了(因为在j才发生匹配失效,因此j前面的都已经匹配到了),所以本质上是让模式串的重复后缀串和前缀串对齐了,假设模式串的长度是Length,那么,
原先对其的元素是j,j匹配失效后,才让j指向next[j],因此模式串是从:j和i对齐 -> k和i对齐
其实移动了:j-k 的距离,而不是k的距离
2.6 KMP算法完整代码
int KmpSearch(char* s, char* p)
{
int i = 0;
int j = 0;
int sLen = strlen(s);
int pLen = strlen(p);
while (i < sLen && j < pLen)
{
//①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++
if (j == -1 || s[i] == p[j])
{
i++;
j++;
}
else
{
//②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j]
//next[j]即为j所对应的next值
j = next[j];
}
}
if (j == pLen)
return i - j;
else
return -1;
}
Refre:https://blog.csdn.net/v_july_v/article/details/7041827