暴力匹配算法是指从主串(str)和子串(sub)的第一个字符开始,将这两个字符进行比较,如果这两个字符不匹配(不相等),主串需要回溯(回溯的位置为主串当前匹配字符位置-子串当前匹配位置+1),然后子串回到起始位置,然后再次比较两个字符的大小,一直到子串字符全部匹配成功。
下面为暴力比配算法的演示过程:
假设现在有主串holleworld,子串ewo,
起始状态:
此时,发现当前主串的字符与子串的字符不匹配,此时子串与主串需要回溯,即都需要向后移动
不匹配,后移
此时,主串的字符与子串的字符相匹配,继续匹配后续字符
主串的字符与子串的字符相匹配,继续匹配后续字符
此时,子串字符都匹配完毕,匹配结束
接下来,我们看代码
首先我们需要定义一个结构(类),用来表示一个串,还有所需要的函数
class Bunch
{
public:
//类成员函数指针
using Tabulation = int (Bunch::*)(const char *sub);
//类型别名
using type = int;
enum Chiose
{
FORCE,//暴力匹配算法
KMP//KMP算法
};
private:
//函数表
static Tabulation Menu[];
private:
char *str; //存储字符串
size_t size; //字符串长度
private:
//构造字符串
char *Create(const char *s);
//获取字符串长度
size_t GetSize(const char *s);
//暴力匹配算法
int Substring1(const char *s);
//KMP算法
int Substring2(const char *s);
//求辅助数组
type *CreateNext(const char *sub);
public:
Bunch(const char *s);
Bunch() = default;
~Bunch();
void Push_Back(const char *s);
//打印字符串
void Print();
//判断该串中是否存在子串
int Substring(const char *sub, Chiose chiose);
public:
//重载输出流,作用于Print()一样
friend std::ostream &operator<<(std::ostream &out, const Bunch &str);
};
现在我们给出其函数定义
//初始化函数表
Bunch::Tabulation Bunch::Menu[] = {
&Bunch::Substring1,
&Bunch::Substring2
};
std::ostream &operator<<(std::ostream &out, const Bunch &str)
{
for (size_t n = 0; n < str.size; ++n)
{
out << str.str[n];
}
return out;
}
//输出字符串
void Bunch::Print()
{
for (size_t n = 0; n < this->size; ++n)
{
std::cout << this->str[n];
}
}
Bunch::~Bunch()
{
if (this->str)
{
delete[] this->str;
}
this->str = nullptr;
this->size = 0;
}
Bunch::Bunch(const char *s)
{
this->str = this->Create(s);
this->size = this->GetSize(s);
}
//拷贝字符串
bool Bunch::Copy(char *s1, const char *s2)
{
while (*s2 != '\0')
{
*s1 = *s2;
++s1;
++s2;
}
return true;
}
//拷贝字符串
bool Bunch::Copy(char *s1, const char *s2, size_t start)
{
while (*s2 != '\0')
{
s1[start] = *s2;
++s2;
++start;
}
return true;
}
//获取字符串长度
size_t Bunch::GetSize(const char *s)
{
size_t len = 0;
while (*s != '\0')
{
++len;
++s;
}
return len;
}
//分配字符串内存
char *Bunch::Create(const char *s)
{
size_t len = this->GetSize(s);
char *pNew = new char[len];
try
{
if (!pNew)
{
throw "create 内存分配失败";
}
}
catch (const char *string)
{
std::cerr << string << std::endl;
return nullptr;
}
this->Copy(pNew, s);
return pNew;
}
下面是暴力匹配算法的代码实现
//字符串暴力匹配算法
int Bunch::Substring1(const char *s)
{
const size_t subLen = this->GetSize(s);
size_t subP = 0, //记录子串的位置
mainP = 0; //记录主串的位置
while (mainP != this->size && subP != subLen)
{
//主串字符与子串字符匹配
if (this->str[mainP] == s[subP])
{
//向后移动,继续匹配下面字符
++subP;
++mainP;
}
//主串字符与子串字符不匹配
else
{
//主串回溯
mainP = mainP - subP + 1;
//子串回到起始位置
subP = 0;
}
}
//子串位置为子串的末尾,说明该主串中存在该子串,返回子串出现的起始位置
if (subLen == subP)
return mainP - subP;
//没有找到该子串
else
return -1;
}
首先我们先介绍一个概念——公共前后缀。
公共前后缀:字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。 例如对于字符串 abacaba,其前缀有 a, ab, aba, abac, abacab,后缀有bacaba, acaba, caba, aba, ba, a。
公共前后缀长度:下面再以”ABCDABD”为例,进行介绍:
- ”A”的前缀和后缀都为空集,共有元素的长度为0;
- ”AB”的前缀为[A],后缀为[B],共有元素的长度为0;
- ”ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;
- ”ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;
- ”ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;
- ”ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为2;
- ”ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。
KMP 算法是一种改进的字符串匹配算法,由 D.E.Knuth,J.H.Morris 和 V.R.Pratt 提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称 KMP 算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个 next() 函数实现,函数本身包含了模式串的局部匹配信息。KMP 算法的时间复杂度 O(m+n)。这种算法的思想是为了让不必要的回溯不发生
KMP算法演示
假设现在有主串abcabeabc,子串abea
算法开始:
先求得主串的公共前后缀长度表,如图所示(蓝色部分)
此时主串字符与子串字符匹配
继续比较后续字符:
注意,此时主串不用回溯,子串需要回溯,回溯的位置next中的数字(这是KMP算法与暴力匹配算法不同的地方)
此时主串字符与子串字符不匹配
继续比较后续字符:
注意,由于此时对应的next数组中是0,说明此时主串也需要向后移动,否则就会陷入匹配不成功的死循环,
此时主串字符与子串字符匹配成功,
如图所示,后面子串的匹配肯定都是成功的,在此小编就不为大家继续演示了
接下来我们来看看KMP算法的核心,即next数组是怎么构建的,大家请看代码
//构造Next数组(KMP算法用)
Bunch::type *Bunch::CreateNext(const char *str)
{
//计算子串长度
const size_t len = this->GetSize(str);
//申请内存
Bunch::type *next = new Bunch::type[len];
next[0] = -1;
Bunch::type begin = 0, //子串起始位置
end = -1; //子串结尾位置
//计算公共前后缀
while (begin < len)
{
//避免数组下表为-1,字符相等匹配后续字符
if (-1 == end || str[begin] == str[end])
{
++begin;
++end;
//部分匹配元素的长度
next[begin] = end;
}
else
{
//重置end
end = next[end];
}
}
return next;
}
知道如何构建next数组后,让我们来看看KMP算法的实现吧
// KMP算法
int Bunch::Substring2(const char *s)
{
Bunch::type *next = this->CreateNext(s);
const size_t subLen = this->GetSize(s);
size_t subP = 0, //记录子串的位置
mainP = 0; //记录主串的位置
while (mainP != this->size && subP != subLen)
{
//主串字符与子串字符匹配
if (this->str[mainP] == s[subP] || -1 == subP)
{
//向后移动,继续匹配下面字符
++subP;
++mainP;
}
//主串字符与子串字符不匹配
else
{
//子串回溯
subP = next[subP];
}
}
//记得释放next数组内存,防止内存泄漏
delete []next;
//子串位置为子串的末尾,说明该主串中存在该子串,返回子串出现的起始位置
if (subLen == subP)
return mainP - subP;
//没有找到该子串
else
return -1;
}
接下来,还差最后一步,完善我们的suustring()函数,
int Bunch::Substring(const char *sub, Chiose chiose = Bunch::FORCE)
{
//以下为2中调用函数的方式,大家可以根据自己的喜好选择
// return (this->*Menu[chiose])(sub);
auto funP = Bunch::Menu[chiose];
return (this->*funP)(sub);
}
最后我们来测试以下,下面是主函数,
int main()
{
Bunch string("abcabeabc");
std::cout << string << std::endl;
//暴力匹配算法结果
std::cout << "位置:" << string.Substring("abea", Bunch::KMP) << std::endl;
//KMP算法结果
std::cout << "位置:" << string.Substring("abea", Bunch::FORCE) << std::endl;
return 0;
}