这篇是 算法 类别中的第一篇,大二学的数据结构,平时写项目也几乎没有用过,很多常见算法的实现都不能记清了。
以后每天复习点,然后写写对复习到的数据结构和算法的理解。
我们先来看一种简单的好理解的字符串匹配算法,这里我用c语言实现这个算法
#include<stdio.h>
#include<Windows.h>
using namespace std;
int Index(char *, char *);
int getStrLength(char *);
int main(){
int res = Index("acabaabaabcacaabc", "abaabcac");
printf("position = %d\n", res);
system("pause");
return 0;
}
/*
*字符串匹配函数
*S为主串 该例子中为 cabcabb
*T为模式串 该例子中为 abb
*/
int Index(char *S, char *T){
//i,j 分别为要匹配的S,T字符的位置
int i = 0;
int j = 0;
//sLength,tLength 分别为S,T的长度
int sLength = getStrLength(S);
int tLength = getStrLength(T);
/*
*如果主串匹配到尽头 i >= sLength 这种情况就是主串匹配完了,假设这时候j < tLength 说明未匹配上
*或者模式串匹配到尽头 j >= tLength 这种情况就是模式串匹配完毕,即匹配上
*/
while (i < sLength && j < tLength){
//如果当前对应字符匹配,就继续进行下一个字符的比较
if (S[i] == T[j]){
i++;
j++;
}
//否则,即是不对应,这时候主串的i就要退到开始匹配的位置的下一位
//j要退到0位,模式串每次都要从头匹配 !!!(这就是KMP和普通算法的区别)!!!
else{
i = i - j + 1;
j = 0;
}
}
//如果循环结束后 j >= tLength,实际上只可能等于,就说明匹配上了,当前i位置减去T的长度就是匹配上的第一个字符的位置
if (j >= tLength)
return i - tLength;
//否则就返回 -1 标识没有匹配上
else
return -1;
}
//自实现一个函数返回传入字符串的长度
int getStrLength(char *S){
int count = 0;
while (*S != '\0'){
S ++;
count ++;
}
return count;
}
算法的最坏情况复杂度为O(n*m) n,m为两个串的长度
中间打!!!的部分已经提到了普通匹配算法的劣势,我们举例说明一下
上述算法我们传入的
主串为 acabaabaabcacaabc 模式串为 abaabcac
下面我们用S,T来表示主串和模式串
假设匹配到下面的这个位置(上下对应,S[7]和T[5]比较 *记住我们是从0开始的*)
第n次匹配
S acabaabaabcacaabc
T abaabc
这时候没有匹配上,如果按上述算法我们就要从S[3]开始和T[0]比较
S acabaabaabcacaabc
T a
这样的话,只要没有匹配上,之前匹配的操作都等于白执行了而且,在很长的文本匹配中,可能主串中多次出现和模式串部分匹配的字段
有没有办法利用上之前的匹配操作呢?
KMP算法就是基于这个思想,利用上“部分匹配”的记过将模式串尽可能的向右“滑动够远的距离
例,第n次匹配后就可以这样
S acabaabaabcacaabc
T aba
为什么可以这样呢?
我们观察模式串 发现模式串中有重复的字段(单个重复字符其实也可以利用上的,这里我们拿好理解的来)
abaabcac
我们从第n次匹配就可以知道 abaab这部分是匹配上的,呢么主串中就含有这部分字符串
直接由S[7]和T[2]比较
为什么可以这样呢?
我们由第n次匹配可以知道 abaab这部分是匹配上的,呢么主串中就含有这部分字符串
呢只要我们对模式串自身进行分析,分析出来模式串自身对于自身的匹配情况
那样,出现这种部分匹配的情况时候,我们的 主串S 的 i 就不用回退
只要求得模式串T从哪个位置开始匹配就可以了
int Index_KMP(char *S, char *T){
int i = 0;
int j = 0;
int sLength = getStrLength(S);
int tLength = getStrLength(T);
int *next = get_next(T);
while (i < sLength && j < tLength)
{
if (j == 0 || S[i] == T[j]){
i ++;
j ++;
}
else{
j = next[j];//和之前的Index函数对比,这个地方我们并没有回退i,只是通过一个next数组,得出T[j]从哪里开始
}
}
if (j >= tLength)
return i - tLength;
else
return -1;
}
下面就是KMP算法的重点,求出next[]数组
int* get_next(char *T){
//i标识T当前字符的位置
int i = 0;
//j标识前一个字符的next[i]
int j = -1;
int tLength = getStrLength(T);
//动态数组,长度为T的长度
int *next = (int*)malloc(sizeof(int) * tLength);
//我们设模式串第一个字符的next[] = -1
//因为当模式串第一个字符就和主串不匹配时候,必然是直接去匹配主串下一个字符
next[0] = -1;
while (i < tLength){
/*
* j == -1的时候,说明模式串第一个字符就匹配不上主串,这时候就应该和主串下一个字符匹配,所以i++; *
* 而且next[1]也肯定是0,就是说当模式串第二个字符匹配失败时候,我们肯定是直接从模式串第一位重新匹配的
* 这里我们把next[0] = -1的优势在于建立了一种统一。具体下面阐述
*/
if (j == -1 || T[i] == T[j]){
i ++;
j ++;
next[i] = j;
}
/*
*这里我们为什么让j等于next[j]呢?请大家思考一下,下面我会有解释
*/
else{
j = next[j];
}
}
return next;
}
这里我建立一个对应的表
i 0 1 2 3 4 5 6 7
模式 a b a a b c a c
next[i] -1 0 0 1 1 2 0 1
我们根据这个表来逐步分析一下这个算法
这个算法就是根据当前字符是否和自身匹配来得出来下一个字符的next[]
首先next[0] = -1 ,next[1] = 0
当i = 1时候 ,T[1] != T[0] (T[0]中的0来自于next[1] 后面都是如此)
如果和第0个都不匹配的话,就没办法向后滑动,所以next[2] = 0 只能老老实实从第一个开始匹配
当i = 2时候,T[2] == T[0],说明这个字符和第一个字符相等
那样的话,T[3]匹配出现错误时候,因为T[3]前一个字符和第一个字符相等了,那么这部分就不用比对了,直接比对T[1]和主串当前字符就可以了
当i = 3时候 T[3] != T[1] *这里我们要详尽说明一下为什么 else 时 j = next[j]*
i = 3时候可以用下图表示
0 1 2 3 4 5 6 7
模式串自身作为主串 T a b a a b c a c
模式串和自身进行匹配 T1 a b
这时候为什么要 j = next[j]呢?
因为 T[3]和T1[next[3]]不匹配,我们就这时候把T1当成一个模式串,T当成一个主串
T1[1]不匹配了,这时候就应该让 T1[next[1]] 和主串进行比对
这里就充分体现了自身和自身比对去找出规律
这时候T[3] == T1[0] 这种情况就和i = 2时候一样了
之后的分析,大家可以自己进行一下,前面三个分析就已经包含所有情况了。
虽然KMP算法最坏情况也是 O[n*m] 但是但实际的字符串匹配中,部分匹配的情况很多,而且我们主串根本不用回退,所以它的执行效率还是不错的
第一次去分析算法,才发现自己当初学的时候,对算法的理解并不透彻,很难去阐述出来为什么要这么做,
这也体现了,看懂和能自己写,自己写和给别人讲明白,真的是不同的境界
自己足足斟酌了几个小时,才写下来自己的分析,而且我仍然觉得自己讲的有点不明白
这也坚定了我要对自己以前掌握的算法都一一分析的决心