串
1.逻辑结构
1.1 串:是由零个或多个字符组成的有限序列,又名叫字符串。
1.2 长度为0的字符串称为空串,注意空串不是空格串。
(我们在各种语言中都很熟悉串,所以逻辑结构就到这里了,本章的重点在于串的模式匹配)
2.物理结构
2.1 定长顺序存储
本章的抽象数据类型以及模式匹配算法都基于定长顺序存储。
#include<iostream>
#define OK 1;
#define ERROR 0;
#define MaxSize 200
#define Status int
typedef struct {
char data[MaxSize];
int length;
}SString;
2.2 动态分配顺序存储
#include<iostream>
#define OK 1;
#define ERROR 0;
#define Status int
typedef struct {
char *data;
int length;
}HString;
串的动态存储的应用场景很少,我们熟悉的高级语言对于String的定义大都是定长的,所以此处的抽象数据类型略过不表。
2.4 块链存储
如下图所示,由于串的特殊性,我们可以用块链的方式存储串,就是每个节点存储多个字符。由于本章的重点不在于此,所以此处略过代码实现。
3. 抽象数据类型
3.1 初始化串
/*初始化串为某一输入值*/
Status initSString(SString& Str) {
string s;
cout << "请输入串值:" << endl;
getline(cin,s);
Str.length = s.size();
int i = 0;
for (i; i < s.size(); i++) {
Str.data[i] = s[i];
}
Str.data[i] = '\0';
return OK;
}
3.2 串比较
/*串比较*/
Status StrCompare(SString S, SString T) {
for (int i = 0; i < S.length, i < T.length; i++) {
if (S.data[i] > T.data[i])
return 1;
if (S.data[i] < T.data[i])
return -1;
}
if (S.length > T.length)
return 1;
else if (S.length < T.length)
return -1;
else return 0;
}
然后再写个小实验
int main() {
SString Str1,Str2;
initSString(Str1);
initSString(Str2);
cout << StrCompare(Str1, Str2) << endl;
}
如下:
3.3 串赋值
/*串赋值,将串赋值为string类型的串值*/
Status StrAssign(SString Str, string s) {
Str.length = s.size();
int i = 0;
for (i; i < Str.length; i++) {
Str.data[i] = s[i];
}
Str.data[i] = '\0';
return OK;
}
3.4 串连接
/*串连接,连接S1,S2为串T*/
Status StrConcat(SString& T, SString S1, SString S2) {
T.length = S1.length + S2.length;
int i = 0;
for (i; i < S1.length; i++) {
T.data[i] = S1.data[i];
}
for (i = S1.length; i < T.length; i++) {
T.data[i] = S2.data[i - S1.length];
}
T.data[i] = '\0';
return OK;
}
3.5 求子串
/*求子串 Sub赋值为S从第pos个字符起长度为len的字符串,
注意pos不是索引是第pos个字符*/
Status SubString(SString& Sub, SString S, int pos, int len) {
if (pos + len - 1 > S.length)//没有这么长
return ERROR;
Sub.length = len;
int i = 0;
for (i; i < len; i++) {
Sub.data[i] = S.data[i + pos - 1];
}
Sub.data[i] = '\0';
return OK;
}
3.5 清空串
/*清空串*/
Status ClearString(SString& S) {
StrAssign(S, "");
return OK;
}
小实验
int main() {
SString Str1, Str2, Str3,Str4;
StrAssign(Str1, "My girlfriend is ");
cout << Str1.data << endl;
StrAssign(Str2, "IU");
cout << Str2.data << endl;
cout << "连接" << endl;
StrConcat(Str3, Str1, Str2);
cout << Str3.data << endl;
cout << "子串" << endl;
SubString(Str4, Str3, Str1.length + 1, 2);
cout << Str4.data << endl;
}
4.朴素模式匹配算法
模式匹配:子串的定位操作通常称为串的模式匹配,它求的是子串(通常称为模式串)在主串中的位置。
(朴素,就是暴力解法)
算法的基本思想:
遍历主串字符,直到以某主串字符作为起始位置与模式串长度相等的串与模式串相等。
这样的算法时间复杂度=O(m * n)。如果每次都是最后一个字符与模式串不等,那每次主串字符都要退回很多。
/*朴素模式匹配*/
Status Index(SString S, SString T) {
int i = 0, j = 0;
if (S.length < T.length) return -1;
if (T.length == 0) return -1;
while (i < S.length && j < T.length) {
if (S.data[i] != T.data[j]) {
i = i - j + 1;
j = 0;
}
else {
i++;
j++;
}
}
if (j != T.length) return -1;//没找到
else return i - j;
}
来个很巧妙的实验:
int main() {
SString Str1, Str2, Str3,Str4;
StrAssign(Str1, "My girlfriend is IU");
cout << "主串:";
cout << Str1.data << endl;
StrAssign(Str2, "IU");
cout << "模式串:";
cout << Str2.data << endl;
cout << "idnex:" << endl;
cout << Str1.data+Index(Str1, Str2) << endl;
StrAssign(Str3, "girlfriend");
cout << "模式串:";
cout << Str3.data << endl;
cout << "idnex:" << endl;
SubString(Str4, Str1, Index(Str1, Str3) + 1, Str3.length);
//Index是索引,SbuString的pos是第几个字符 所以要+1
cout << Str4.data << endl;
}
5. KMP算法
5.1 算法基础原理
串的前缀、后缀和部分匹配值。
那这些有什么用呢?我们先来思考一个问题,朴素模式匹配的问题在哪?
例子1:
在朴素模式匹配算法中,此时i应当加1,j应当退回0,来比较S[2]和T[0],S[3]和T[1],S[4]和T[2],S[5]和T[3]。如下图所示:
但是在退回之前,也就是这张图:
我们已经比较过了S[2]和T[1]是相等的,S[3]和T[2]是相等的。
此时,若T[1]T[2]与T[0]T[1]若不相等,S[2]S[3]肯定与T[0]T[1]不等,也就是说i=2不可能是我们想要的结果。
那我们想想什么时候T[1]T[2]与T[0]T[1]相等呢?
T[0]T[1]T[2]
这样排列是不是想到了什么,T[0]T[1]与T[1]T[2]相等就是字符串T[0]T[1]T[2]的前缀T[0]T[1]与后缀T[1]T[2]相等。就是说部分匹配值等于2,但是显然abc的部分匹配值不是2而是0。所以朴素模式匹配的下一步i变成i=2是不行的。
那i再多加1变成i=3呢?
我们已经比较过了S[3]与T[2]是相等的,此时若T[2]与T[0]不等,那显然T[0]与S[3]不等。
那我们想想什么时候T[0]与T[2]相等呢?聪明的你一定想到了,就是字符串T[0]T[1]T[2]的前缀T[0]等于后缀T[2],也就是部分匹配值等于1的时候,但是显然abc的部分匹配值不是1而是0。所以朴素模式匹配的下一步i变成i=3也是不行的。
那i变成i=4呢?由于S[4]与T[4]不相等,我们没办法如前面那样推断,所以i=4是正解。
此时我们考虑一下从i = 1,变成i = 4跨了3步,对了,abc也就是模式串成功匹配的字符长度就是3。
例子2:
对于模式串已经匹配成功的aba,我们很容易得出它的部分匹配值为1。那这个例子中i应该变为多少呢?
同理,我们比较过了T[1]T[2]和S[2]S[3],T[1]T[2]若想与T[0]T[1]相等,也就是S[2]S[3]若想与T[0]T[1]相等,aba的部分匹配值必须为2,显然aba的部分匹配值为1。所以i = 2不行。
那i = 3呢。S[3]和T[2]相等,aba的部分匹配值为1,所以T[0]与T[1]相等,所以S[3]与T[0]相等,所以i = 3正解!
我们分析一下,从i=1到i=3,垮了2步,模式串成功匹配的长度是3,模式串成功匹配部分的部分匹配值是1,也就是:
跨越的步数=模式串成功匹配的长度-模式串部分匹配值
总结:
移动位数 = 已匹配的字符数 - 对应部分的部分匹配值
5.2 基础原理代码化
由上述分析我们知道,为了简化朴素模式匹配算法我们需要得到模式串的部分匹配值。我们把模式串的部分匹配值存储在数组里。
此时的算法我们可以表述为(主串索引i,模式串索引j):i+=j-next[j-1],j=0
此时串的字符的索引与部分匹配值的索引是对应的,但是上述例子中,j=3时发现不匹配,已经匹配的最后一个字符是j = 2。所以我们需要PM[2]=1 这个部分匹配值,为了方便我们将PM数组的元素全部向右移动一位。
注意到我们把名字换成了next,next[0] = -1,
此时的算法我们可以表述为(主串索引i,模式串索引j):i+=j-next[j],j=0
为了方便大家理解部分匹配值的作用,上述算法中每次都移动i和j,但实际上i是可以不动的,看下面这个例子:
此时i = 4, j = 3我们发现不匹配了,就是说i<4的串S元素我们已经比较过了,T[0]T[1]T[2] = S[1]S[2]S[3],由于next[3] = 1;T[0] = T[2]=S[3]。所以接下来我们比较S[4]和T[2]就好了。也就是j = next[j].
此时算法表述为:j = next[j].
(这个数据结构系列都是从索引0开始,有的书从索引1开始,所以会把next数组整体加1,因为这些书的串的物理结构都是S[0]不存放元素,但是我们S[0]是存放元素的,做题时大家自己分辨一下。)
5.3 求next数组
(前言:next数组手动求很简单,但是算法可能不太容易理解,至少比模式匹配难, 这里我们先给出求解方法,再给出代码,最后再说原理)
5.3.1 求解方法
如上图例子,next数组初始值next[0]=-1,next[1]=0。此时我们要求next[2]:
我们比较S[2-1]和S[next[2-1]]即比较S[1]和S[0],不等,由于next[0]==-1。所以next[2] = 0。
如下图:
此时我们求next[3]:
比较 S[3-1]和 S[next[3-1]]即比较S[2]和S[0],相等则next[3] = next[2] + 1 = 1。
此时我们求next[4]:
比较S[4-1]和S[next[4-1]]即比较S[3]和S[1],不等,则比较S[3]和S[next[1]]即比较S[3]和S[0],相等则next[4]=next[3]=1。
此时我们求next[5]:
比较S[5-1]与S[next[5-1]]即比较S[4]与S[1],相等,next[5] = next[4]+1=2
此时我们求next[6]:
比较S[6-1]与S[next[6-1]]即比较S[5]与S[2]不等,则S[5]和S[next[2]]比较,即S[5]和S[0]比较,不等且next[0]=-1,next[6]=0
此时我们求next[7]:
比较S[6]和S[next[6]]即比较S[6]与S[0],相等则next[7]=next[6]+1 = 1
此时我们求next[8]:
比较S[7]与S[next[7]]即S[7]与S[1]相等,则next[8]=next[7]+1=2
5.3.2 代码
然后给大家代码:(由于我的参考书都是以索引1开始所以代码看起来很整洁,但是我都是以索引0开始,由于-1的存在不得不加一些判定防止数组的溢出,但是代码逻辑是一样的)
/*求next数组,注意j是什么就很容易理解,j有一些递归的意思*/
void get_next(SString T, int next[]) {
int i,j;
if (T.length > 0) next[0] = -1;
if (T.length > 1) next[1] = 0;
if (T.length < 2) return;
i = 2;
j = next[i - 1];
while (i < T.length) {
if (j == 0) { //若j是0说明S.data[i-1]和S.data[0]比较,则next[i]只能是0或1
if (T.data[i - 1] == T.data[j])
next[i] = 1;
else next[i] = 0;
j = next[i];
i++;
}
else if (T.data[i-1] == T.data[j]) {
next[i] = next[i - 1] + 1;
j = next[i];
i++;
}
else {
j = next[j];
}
}
}
5.3.3 求next数组原理
此时我们求next[5]: 比较S[5-1]与S[next[5-1]]即比较S[4]与S[1],相等,next[5] = next[4]+1=2。
上面是我们给的求解方法,为什么这样求呢?原理如下:
我们求next[5],实际就是求’abaab’的部分匹配值。而我们已经知道了‘a’,‘ab’,‘aba’,'abaa’的部分匹配值分别是0011。
从已知出发,‘abaa’的部分匹配值是1,说明S[0]=S[3],若S[4]=S[1],那S[0]S[1] = S[3]S[4],部分匹配值就是1+1=2了。
显然这里S[4]和S[1]是相等的,这就是我们比较S[j-1]和S[next[j-1]]的原因。
此时我们求next[6]:
比较S[6-1]与S[next[6-1]]即比较S[5]与S[2]不等,则S[5]和S[next[2]]比较,即S[5]和S[0]比较,不等且next[0]=-1,next[6]=0。
为很么比较S[5]和S[2]大家已经知道了,此处S[5]与S[2]不等那为什么比较S[5]和S[next[2]]呢?
我们求next[6]就是求‘abaabc’的部分匹配值,已知’abaab’部分匹配值为2,S[5]!=S[2]那next[6]!=2+1=3。那有没有可能是2呢?
若是2那么S[0]S[1] = S[4]S[5],我们不知道S[5]是否等于S[1],但是我们知道S[0]是不等于S[4]的
**因为next[5]=2,所以’abaab’部分匹配值为2,S[0]S[1]=S[3]S[4],S[0]若等于S[4]则S[0]=S[1],S[0]=S[1]说明next[2]=1。但是next[2]=0所以不可能有S[0]S[1]=S[4]S[5]。**我们根本不用去比较S[1]和S[5],我们只需要比较S[5]和S[0]就够了,这个模式串中next[1]=0那若相等则为1不等则为0。
总的来说,next[j-1]已经限制了next[j]的大小,这有一点迭代递归的意思,我们根据next[j-1]的指引进行比较。
6. kmp代码
在上面已经用了很长的篇幅去说kmp以及求next数组的原理,所以这里直接给出代码。
Status indexKmp(SString S, SString T,int next[]) {
int i = 0,j = 0;
get_next(T, next);
while (i < S.length && j < T.length) {
if (S.data[i] == T.data[j] ) {//相等
i++;
j++;
}
else if(j == 0){//没有已匹配字符
i++;
}
else {//有已匹配字符
j = next[j];
}
}
if (j == T.length) return i - j;
else return -1;
}
写个小实验:
int main() {
SString Str1, Str2, Str3,Str4;
int next[MaxSize];
StrAssign(Str1, "My girlfriend is IU");
StrAssign(Str2, "boy");
cout << "主串\t";
cout << Str1.data << endl;
cout << "模式串\t";
cout << Str2.data << endl;
cout << "kmp结果:\t";
int index = indexKmp(Str1, Str2, next);
if (index == -1) {
cout << "查询无果" << endl;
}
else {
for (int i = index; i < index + Str2.length; i++) {
cout << Str1.data[i];
cout << "\t";
}
cout << "" << endl;
}
StrAssign(Str3, "asdadabaabcaba");
StrAssign(Str4, "abaabcaba");
cout << "主串\t";
cout << Str3.data << endl;
cout << "模式串\t";
cout << Str4.data << endl;
cout << "kmp结果:\t";
int index2 = indexKmp(Str3, Str4, next);
if (index2 == -1) {
cout << "查询无果" << endl;
}
else {
for (int i = index2; i < index2 + Str4.length; i++) {
cout << Str3.data[i];
cout << "\t";
}
cout << "" << endl;
}
}
7. 结语
我以为2个小时就能完成串这一章,但是把kmp说清楚对于我来说需要一些时间,我花了大量篇幅介绍next数组,超出我计划的时间太久了,所以关于kmp算法的优化就略过了。应该不会有考试或者面试让弄一个nextval出来吧,比较我感觉它也没优化多少。