本文介绍两种字符串匹配的方法:BF算法(暴力匹配),KMP算法(进阶)
BF算法
暴力匹配,顾名思义就是一个一个的进行匹配,若匹配成功,则移动到下一位,否则进行回溯
分为主串与子串,从第一位开始校对,若比对失败,则子串回溯,主串移动到下一位
字符串的结构
typedef struct String
{
char* data;//数据域
int len;//串的长度
}String;
字符串的初始化
String是我们自己对结构体设置的别名
1.先new一个指针;并把它的data设为NULL,以防野指针;再设置它的长度为0
String* initString()
{
String* s = new String;
s->data = NULL;
S->len = 0;
return s;
}
字符串赋值
将传入的字符数组赋值给串,我们需要字符数组的长度len,并且要遍历字符数组,所以需要设立工作指针temp
void stringAssign(String* s, char* x)
{
int len = 0;//记录字符数组x的长度
char* temp = x;//设置工作指针来遍历数组获得长度
while (*temp)
{
len++;
temp++;
}
if (len == 0)//则传入的是空串
{
s->data = NULL;
s->len = 0;
}
else
{
temp = x;//复位temp
s->len = len;
s->data = new char[len];//给串的数据域new一个len长的字符数组
for (int i = 0; i < len; i++)//将传入的字符数组赋给串
{
s->data[i] = *temp;
temp++;
}
}
}
打印字符串
void printString(String* s)
{
for (int i = 0; i < s->len; i++)
{
cout << s->data[i] << " ";
}
cout << endl;
}
BF匹配
重点:主串的回溯与子串的回溯
int BF(String* master, String* sub)
{
int i = 0;//主串索引下标
int j = 0;//子串索引下标
while (i < master->len && j < sub->len)
{
if (master->data[i] == sub->data[j])//若主串与子串都匹配成功,则同时移动一位
{
i++;
j++;
}
else//如果匹配失败
{
i = i - j + 1;//主串回溯到初始位置的下一位
j = 0;//子串回溯到第一个
}
}
if (j == sub->len)//若j==sub->len,则代表子串已经成功匹配玩所有的字符
{
return 1;
}
else
{
return 0;
}
}
完整的BF算法
#include<iostream>
#include<stdlib.h>
#include<cstring>
using namespace std;
typedef struct String
{
char* data;//数据域
int len;//串的长度
}String;
String* initString()//初始化
{
String* s = new String;
s->data = NULL;//防止野指针
s->len = 0;//初始化为0
return s;
}
void stringAssign(String* s, char* x)
{
int len = 0;//记录字符数组x的长度
char* temp = x;//设置工作指针来遍历数组获得长度
while (*temp)
{
len++;
temp++;
}
if (len == 0)//则传入的是空串
{
s->data = NULL;
s->len = 0;
}
else
{
temp = x;//复位temp
s->len = len;
s->data = new char[len];//给串的数据域new一个len长的字符数组
for (int i = 0; i < len; i++)//将传入的字符数组赋给串
{
s->data[i] = *temp;
temp++;
}
}
}
void printString(String* s)
{
for (int i = 0; i < s->len; i++)
{
cout << s->data[i] << " ";
}
cout << endl;
}
int BF(String* master, String* sub)
{
int i = 0;//主串索引下标
int j = 0;//子串索引下标
while (i < master->len && j < sub->len)
{
if (master->data[i] == sub->data[j])//若主串与子串都匹配成功,则同时移动一位
{
i++;
j++;
}
else//如果匹配失败
{
i = i - j + 1;//主串回溯到初始位置的下一位
j = 0;//子串回溯到第一个
}
}
if (j == sub->len)//若j==sub->len,则代表子串已经成功匹配玩所有的字符
{
return 1;
}
else
{
return 0;
}
}
int main()
{
String* s1 = initString();
String* s2 = initString();
char s11[20], s22[20];
cin >> s11 >> s22;
stringAssign(s1, s11);
stringAssign(s2, s22);
printString(s1);
printString(s2);
cout<<BF(s1, s2);
}
KMP算法
在BF算法上的提升:主串不进行回溯,子串回溯;
子串回溯的位置由next数组来确定 ;next[i]的值表示下标为i的字符前的字符串最长相等前后缀的长度;也表示该处字符不匹配时应该回溯到的字符的下标
next数组是由子串来决定的,了解next数组之前我们先要了解前缀与后缀以及最长相等前后缀
比如:字符串abcdab
前缀的集合:{a,ab,abc,abcd,abcda}
后缀的集合:{b,ab,dab,cdab,bcdab}
那么最长相等前后缀不就是ab嘛
接下来我们来模拟一下KMP匹配
第一个长条代表主串,第二个长条代表子串
红色部分代表两串中已匹配的部分,绿色和蓝色部分分别代表主串和子串中不匹配的字符
现在发现了不匹配的地方,根据KMP的思想我们要将子串向后移动,现在解决要移动多少的问题;之前提到的最长相等前后缀的概念有用处了。因为红色部分也会有最长相等前后缀。如下图:
灰色部分就是红色部分字符串的最长相等前后缀;
我们子串移动的结果就是让子串的红色部分最长相等前缀和主串红色部分最长相等后缀对齐。
这一步弄懂了,KMP算法的精髓就差不多掌握了。接下来的流程就是一个循环过程了。
每一个字符前的字符串都有最长相等前后缀,而且最长相等前后缀的长度是我们移位的关键,所以我们单独用一个next数组存储子串的最长相等前后缀的长度。
所以next[i]=j,含义是:下标为i 的字符前的字符串最长相等前后缀的长度为j。
我们可以算出,子串t= "abcabcmn"的next数组为:
next[0]=-1(前面没有字符串单独处理);
next[1]=0(第二个字符前面只有一个字符,不存在最长相等的前后缀,所以为0);
next[2]=0(c的前面有ab,前缀为a后缀为b,故没有最长相等前后缀,所以为0);
next[3]=0(a的前面有abc,前缀有ab后缀有bc,或者前缀有a后缀有c,但是两个都不是相等的,所以没有最长相等的前后缀,所以为0);
next[4]=1(b的前面有abca,前缀有abc,ab,a后缀有bca,ca,a,发现前后缀有相等的(前缀a=后缀a),所以最长相等的前后缀长度为1)
next[5]=2(c的前面有abcab,前缀有abca,abc,ab,a后缀有bcab,cab,ab,a,发现前后缀有相等的最大的是(前缀ab=后缀ab),所以最长相等的前后缀长度为2)
next[6],next[7]同理
a | b | c | a | b | c | m | n |
---|---|---|---|---|---|---|---|
next[0] | next[1] | next[2] | next[3] | next[4] | next[5] | next[6] | next[7] |
-1 | 0 | 0 | 0 | 1 | 2 | 3 | 0 |
获得next数组
int* getNext(String* sub)
{
int* next = new int[sub->len];//new一个子串那么长的next数组
int i = 0;//子串的索引下标
int j = -1;//next数组的索引下标
next[i] = -1;//next数组的第一个元素单独处理
while (i < sub->len-1)//因为第一个不用处理,所以i小于len-1就行了
{
if (j == -1 || sub->data[i] == sub->data[j])
{
//next[0]=-1跳过了
//next[1]=0,因为第二个字符前面只有一个字符,所以不存在最长相等前后缀的长度
//next[i]保存了第j个字符的最长相等前后缀的长度
i++;
j++;
next[i] = j;
}
else
{
j = next[j];//next[j]的值表示下标为j的字符 前的 字符串最长相等前后缀的长度。
//匹配失败时,next[j]的值就是应该回溯到的字符的下标,并赋值给j
}
}
return next;
}
打印next数组
void printNext(int* next, int len)
{
cout << "Next数组:";
for (int i = 0; i < len; i++)
{
cout << next[i] << " ";
}
cout << endl;
}
KMP匹配
匹配过程类似BF暴力,但是主串不回溯,且子串回溯的值由next数组来确定
int KMP(String* master, String* sub, int* next)
{
int i = 0;//主串的下标索引
int j = 0;//子串的下标索引
while (i < master->len && j < sub->len)
{
if (j == -1 || master->data[i] == sub->data[j])
{
i++;
j++;
}
else//匹配失败
{
j = next[j];//子串进行回溯,回溯到的位置就是此时next[j]的值
}
}
if (j >= sub->len)//如果子串的j匹配完成
{
return i - sub->len;//返回子串匹配成功在主串的数组下标
}
else
{
return -1;
}
}
完整的KMP匹配
#include<iostream>
using namespace std;
typedef struct String
{
char* data;//存放串的数据域
int len;//串的长度
}String;
String* initString()
{
String* s = new String;
s->data = NULL;//让data赋初值为NULL
s->len = 0;//串的长度初始化为0
return s;
}
void stringAssign(String* s, char* x)
{
if (s->data)//如果new出来的data不为空,就先delete掉
{
delete s->data;
}
else//new出来的data为空,可以赋初值x
{
int len = 0;//记录传入字符数组的长度
char* temp = x;//设置工作指针来遍历x字符数组,以求得x的长度
while (*temp)//只要*temp不为空就循环下去
{
len++;
temp++;//temp移动到下一位
}
if (len == 0)//若传入的字符串为空串
{
s->data = NULL;
s->len = 0;
}
else
{
temp = x;//重新复位工作指针
s->len = len;//将字符串的长度赋值给主串
s->data = new char[len + 1];//len+1给主串开辟空间
for (int i = 0; i < len; i++)//将字符数组赋值给主串
{
s->data[i] = *temp;
temp++;
}
}
}
}
void printString(String* s)
{
for (int i = 0; i < s->len; i++)
{
cout << s->data[i] << " ";
}
cout << endl;
}
//next[i]的值表示下标为i的字符前的字符串最长相等前后缀的长度。
//表示该处字符不匹配时应该回溯到的字符的下标
int* getNext(String* sub)
{
int* next = new int[sub->len];//new一个子串那么长的next数组
int i = 0;//子串的索引下标
int j = -1;//next数组的索引下标
next[i] = -1;//next数组的第一个元素单独处理
while (i < sub->len-1)//因为第一个不用处理,所以i小于len-1就行了
{
if (j == -1 || sub->data[i] == sub->data[j])
{
//next[0]=-1跳过了
//next[1]=0,因为第二个字符前面只有一个字符,所以不存在最长相等前后缀的长度
//next[i]保存了第j个字符的最长相等前后缀的长度
i++;
j++;
next[i] = j;//
}
else
{
j = next[j];//next[j]的值表示下标为j的字符 前的 字符串最长相等前后缀的长度。
//匹配失败时,next[j]的值就是应该回溯到的字符的下标,并赋值给j
}
}
return next;
}
void printNext(int* next, int len)
{
cout << "Next数组:";
for (int i = 0; i < len; i++)
{
cout << next[i] << " ";
}
cout << endl;
}
int KMP(String* master, String* sub, int* next)
{
int i = 0;//主串的下标索引
int j = 0;//子串的下标索引
while (i < master->len && j < sub->len)
{
if (j == -1 || master->data[i] == sub->data[j])
{
i++;
j++;
}
else//匹配失败
{
j = next[j];//子串进行回溯,回溯到的位置就是此时next[j]的值
}
}
if (j >= sub->len)//如果子串的j匹配完成
{
return i - sub->len;//返回子串匹配成功在主串的数组下标
}
else
{
return -1;
}
}
int main()
{
String* s1 = initString();
String* s2 = initString();
char a[] = "abcabeabcabcmn";
char b[] = "abcabcmn";
stringAssign(s1, a);
stringAssign(s2, b);
printString(s1);
printString(s2);
int* next = getNext(s2);
printNext(next, s2->len);
cout <<"子串在主串中的位置(数组下标):"<< KMP(s1, s2, next) << endl;
}