- 数据结构 DATA STRUCTURE
- 四、串
- 4.1 串的存储结构
- 4.2 串的基本操作
- 4.3 串的模式匹配
- 4.3.1 模式匹配算法--朴素模式匹配(暴力破解BF)
- 简介
- 实现
- 4.3.2 模式匹配算法--KMP算法
- 简介
- 实现
- *关于顺序存储的串首位是否存储字符引发的KMP算法问题
-
顺序存储
// 串的顺序存储 typedef struct SequentialString{ char ch[MAXSIZE]; // 设定ch[0]不存放值,方便KMP算法 int length; // 串的实际长度,不包括' ' }SqString; -
链式存储
// 串的链式存储 typedef struct StringNode{ char ch[4]; // 用ch[4],而不用ch:提升存储密度(指针next不存储有效数据但占4B内存) struct StringNode* next; }StringNode, *String;
// 初始化串
void StrAssign(SqString &S, char chars[]) {
int i;
for(i = 0; chars[i] != ' '; i++) // 循环赋值,ch[0]不存放值
S.ch[i+1] = chars[i];
S.length = i; // 执行了 i++ 后的i
}
// 输出串
void StrDisp(SqString S) {
if(S.length > 0) {
for(int i = 1; i <= S.length; i++)
cout << S.ch[i];
cout << endl;
}
}
// 串判空
bool StrEmpty(SqString S) {
if(S.length > 0)
return true;
return false;
}
// 串比较 S>T,返回值>0;S
for(int i = 1; i <= S.length && i <= T.length; i++) {
if(S.ch[i] != T.ch[i])
return S.ch[i]-T.ch[i];
}
}
// 找指定子串 用Sub返回串S的第pos个字符起长度为len的子串
bool SubString(SqString &Sub, SqString S, int pos, int len) {
if(pos+len-1 > S.length)
return false;
for(int i = pos; i < pos+len; i++)
Sub.ch[i-pos+1] = S.ch[i]; // 如果ch从0开始时:将S.ch[pos-1 ~ pos-1+len] 赋值到 Sub[0 ~ len-1];ch从1开始: S.ch[pos ~ pos+len];画图便知
Sub.length = len;
return true;
}
// void ClearString(SqString &S)
// void DestoryString(SqString &S)
// void ReverseString(SqString &S)
// void StrCopy(SqString &S, SqString T)
// void Contact(SqString &S, SqString S1, SqString S2)
4.3 串的模式匹配
4.3.1 模式匹配算法–朴素模式匹配(暴力破解BF)
简介
从主串第一个字符开始与模式串(子串)比较,相同则比较下一个字符,直到模式串全部匹配成功,返回子串在主串中的位置;其中,字符匹配失败时,返回开始比较的字符下一位置,继续慢慢比较。(主串一直回溯,时间复杂度高。O(mn))
实现// 朴素模式匹配算法1
int IndexSubstring_BF1(SqString S, SqString T) {
int i=1, j=1; // 分别指向主串和子串(模式串)某字符的指针
while (i <= S.length && j <= T.length) {
if (S.ch[i] == T.ch[j]) { // 当前字符相等则继续比较下一个字符
++i;
++j;
}
else {
i = i-j+2; // 主串指针回退到此次与子串比较的开始位置的下一位i = i-(j-1)+1;(ch[]从0开始的数据结构对应为 i = i-j+1;)
j = 1; // 子串指针回退到首位
}
}
if (j > T.length)
return i-T.length; // 子串的位置
else
return -1;
}
// 朴素模式匹配算法2--用第三个变量k存储主串开始比较的位置
int IndexSubstring_BF2(SqString S, SqString T) {
int k = 1; // 用k来标记主串每次开始比较的位置
int i = k, j = 1;
while (i <= S.length && j <= T.length) {
if (S.ch[i] == T.ch[j]) {
++i;++j;
}
else {
++k;
i = k;
j = 1;
}
}
if (j > T.length)
return k;
else
return -1;
}
4.3.2 模式匹配算法–KMP算法
简介
KMP:失配时,利用模式串本身结构特点–最长相等前后缀长度,直接移动子串到相应位置(这个位置保存在next数组)与主串失配位置的字符继续比较,实现主串不回溯,减少时间浪费。(O(m+n))
next数组:利用模式串本身的结构–最长相等前后缀长度计算出来的数组。
next[j]:在子串的第j个字符与主串发生失配时,则跳到子串的next[j]位置重新与主串当前位置进行比较。
求next数组算法 get_next() 图解:
// KMP模式匹配算法
// 计算next数组
void get_next(SqString T, int next[]) {
int i = 1, j = 0;
next[1] = 0; // next数组的第一位必为0 ;next[0]不存放值
while(i < T.length) { // 这里不需要<=,因为i=7时,执行的是next[8]=xxx;
if(j==0 || T.ch[i]==T.ch[j])
next[++i] = ++j; // 若 Pi=Pj, next[j+1]=next[j]+1 (利用上一个字符的next值--也就是上一个字符对应的最长相等前后缀,多一对字符相等,故next比上一个next+1)
else
j = next[j]; // 否则令j=next[j],循环继续 (套娃)
}
cout << "next[]: ";
for(i = 1; i <= T.length; i++)
cout << next[i] << " ";
cout << endl;
}
// KMP
int IndexSubstring_KMP(SqString S, SqString T) {
int i = 1, j = 1;
int next[MAXSIZE]; // 长度比T.length大就行
get_next(T, next);
while(i <= S.length && j <= T.length) {
if(j==0 || S.ch[i]==T.ch[j]) {
++i;++j;
}
else
j = next[j];
}
if (j > T.length)
return i-T.length;
else
return -1;
}
某些情况下之前的next数组并不是最好的,比如aaaab,在第4个a发生失配,j=next[4]=3,此时字符还是a;但是已经知道a会失配,挪到第3个a,还是失配!所以此时需要将对应next[j]修正为next[next[j]](对应next的next),直至两者不相等为止。
// KMP优化--计算nextval
void get_nextval(SqString T, int nextval[]) {
int i = 1, j = 0;
nextval[1] = 0;
while(i <= T.length) {
if(j==0 || T.ch[i]==T.ch[j]) {
++i; ++j;
if(T.ch[i] != T.ch[j]) // 核心改动
nextval[i] = j;
else
nextval[i] = nextval[j];
}
else
j = nextval[j];
}
}
*关于顺序存储的串首位是否存储字符引发的KMP算法问题参考资料:
- 凡三岁爱学习. KMP算法之求next数组代码讲解_哔哩哔哩_bilibili(我称之为b站最通透讲解)
- 王道论坛. 2023数据结构考研复习指导[M]. 北京:电子工业出版社,2021.
T.ch[i] == T.ch[j]:本来该是ch[2]比较ch[1]–第1个字符和第2个字符比较,而因为我的ch[]从0开始,导致我的是第2个和第3个比较,而因为第1个字符和第3个字符一样 (模式串:"ababacb"),所以我还以为算法正确执行中!……直到i=3,j=1才发现不对!!
图中的get_next算法,要求ch[]从1开始。
另外也不用担心从1开始,ch[0]会与ch[1]比较,算法会直接跳过这个的,因为当j=0时满足另一个条件j==0。
调试截图:
参考资料:
- 关于DevC++如何调试的问题,还不会调试的同学看这里—>>>超级详细调试教程,手把手教你如何调试_落雨归林的博客-CSDN博客_devc++调试
- dev c++怎么调试查看数组变量 - CSDN
全部代码:(可直接运行)
#include#include using namespace std; #define MAXSIZE 255 // 串的存储结构 typedef struct SequentialString{ char ch[MAXSIZE]; // 设定ch[0]不存放值,方便KMP int length; // 串的实际长度,不包括' ' }SqString; // 初始化串 void StrAssign(SqString &S, char chars[]) { int i; for(i = 0; chars[i] != ' '; i++) // 循环赋值,ch[0]不存放值 S.ch[i+1] = chars[i]; S.length = i; // 执行了 i++ 后的i } // 输出串 void StrDisp(SqString S) { if(S.length > 0) { for(int i = 1; i <= S.length; i++) cout << S.ch[i]; cout << endl; } } // 串判空 bool StrEmpty(SqString S) { if(S.length > 0) return true; return false; } // 串比较 S>T,返回值>0;S for(int i = 1; i <= S.length && i <= T.length; i++) { if(S.ch[i] != T.ch[i]) return S.ch[i]-T.ch[i]; } } // 找指定子串 用Sub返回串S的第pos个字符起长度为len的子串 bool SubString(SqString &Sub, SqString S, int pos, int len) { if(pos+len-1 > S.length) return false; for(int i = pos; i < pos+len; i++) Sub.ch[i-pos+1] = S.ch[i]; // 如果ch从0开始时:将S.ch[pos-1 ~ pos-1+len] 赋值到 Sub[0 ~ len-1];ch从1开始: S.ch[pos ~ pos+len];画图便知 Sub.length = len; return true; } // void ClearString(SqString &S) // void DestoryString(SqString &S) // void ReverseString(SqString &S) // void StrCopy(SqString &S, SqString T) // void Contact(SqString &S, SqString S1, SqString S2) // 朴素模式匹配算法1 int IndexSubstring_BF1(SqString S, SqString T) { int i=1, j=1; // 分别指向主串和子串(模式串)某字符的指针 while (i <= S.length && j <= T.length) { if (S.ch[i] == T.ch[j]) { // 当前字符相等则继续比较下一个字符 ++i; ++j; } else { i = i-j+2; // 主串指针回退到此次与子串比较的开始位置的下一位i = i-(j-1)+1;(ch[]从0开始的数据结构对应为 i = i-j+1;) j = 1; // 子串指针回退到首位 } } if (j > T.length) return i-T.length; // 子串的位置 else return -1; } // 朴素模式匹配算法2 int IndexSubstring_BF2(SqString S, SqString T) { int k = 1; // 用k来标记主串每次开始比较的位置 int i = k, j = 1; while (i <= S.length && j <= T.length) { if (S.ch[i] == T.ch[j]) { ++i;++j; } else { ++k; i = k; j = 1; } } if (j > T.length) return k; else return -1; } // KMP模式匹配算法 // 计算next数组 void get_next(SqString T, int next[]) { int i = 1, j = 0; next[1] = 0; // next数组的第一位必为0 ;next[0]不存放值 while(i < T.length) { // 这里不需要<=,因为i=7时,执行的是next[8]=xxx; if(j==0 || T.ch[i]==T.ch[j]) next[++i] = ++j; // 若 Pi=Pj, next[j+1]=next[j]+1 (利用上一个字符的next值--也就是上一个字符对应的最长相等前后缀,多一对字符相等,故next比上一个next+1) else j = next[j]; // 否则令j=next[j],循环继续 (套娃) } cout << "next[]: "; for(i = 1; i <= T.length; i++) cout << next[i] << " "; cout << endl; } // KMP int IndexSubstring_KMP(SqString S, SqString T) { int i = 1, j = 1; int next[MAXSIZE]; // 长度比T.length大就行 get_next(T, next); while(i <= S.length && j <= T.length) { if(j==0 || S.ch[i]==T.ch[j]) { ++i;++j; } else j = next[j]; } if (j > T.length) return i-T.length; else return -1; } // KMP优化--计算nextval void get_nextval(SqString T, int nextval[]) { int i = 1, j = 0; nextval[1] = 0; while(i <= T.length) { if(j==0 || T.ch[i]==T.ch[j]) { ++i; ++j; if(T.ch[i] != T.ch[j]) // 核心改动 nextval[i] = j; else nextval[i] = nextval[j]; } else j = nextval[j]; } } // 主函数 int main() { char Str1[] = "aababaabcabaa"; char Str2[] = "abaabcaba"; // 定义串 SqString S, T; // 初始化串 StrAssign(S, Str1); StrAssign(T, Str2); // 输出串 StrDisp(S); StrDisp(T); // 串判空 if(!StrEmpty(S)) cout << "S is Empty" << endl; cout << "S is not Empty!" << endl; // 比较串 cout << "S>T?: " << StrCompare(S, T) << endl; // 模式匹配 cout << IndexSubstring_BF1(S, T) << endl; cout << IndexSubstring_BF2(S, T) << endl; cout << IndexSubstring_KMP(S, T) << endl; }



