目录
数据结构 DATA STRUCTURE
四、串
4.1 串的顺序存储结构实现
-
顺序存储
// 串的顺序存储 typedef struct SequentialString{ char ch[MAXSIZE]; // 设定ch[0]不存放值,方便KMP算法 int length; // 串的实际长度,不包括'\0' }SqString;
-
链式存储
// 串的链式存储 typedef struct StringNode{ char ch[4]; // 用ch[4],而不用ch:提升存储密度(指针next不存储有效数据但占4B内存) struct StringNode* next; }StringNode, *String;
4.2 串的基本操作实现
// 初始化串
void StrAssign(SqString &S, char chars[]) {
int i;
for(i = 0; chars[i] != '\0'; 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<T,返回值<0;
int StrCompare(SqString S, SqString T) {
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)
1. 简介
从主串第一个字符开始与模式串(子串)比较,相同则比较下一个字符,直到模式串全部匹配成功,返回子串在主串中的位置;其中,字符匹配失败时,返回开始比较的字符下一位置,继续慢慢比较。(主串一直回溯,时间复杂度高。O(mn))
2. 实现
// 朴素模式匹配算法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算法
1. 简介
KMP
:失配时,利用模式串本身结构特点–最长相等前后缀长度,直接移动子串到相应位置(这个位置保存在next数组)与主串失配位置的字符继续比较,实现主串不回溯,减少时间浪费。(O(m+n))
next数组
:利用模式串本身的结构–最长相等前后缀长度计算出来的数组。
求next数组算法 get_next() 图解:
next[j]
:在子串的第j个字符与主串发生失配时,则跳到子串的next[j]位置重新与主串当前位置进行比较。
2. 实现
// 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;
}
3. KMP优化
某些情况下之前的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算法之求next数组代码讲解_哔哩哔哩_bilibili(我称之为b站最通透讲解)
- 王道论坛. 2023数据结构考研复习指导[M]. 北京:电子工业出版社,2021.
*关于顺序存储的串首位是否存储字符引发的KMP算法问题
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
。
调试截图:
参考资料:
全部代码:(可直接运行)
代码:
#include <iostream>
#include <cstring>
using namespace std;
#define MAXSIZE 255
// 串的存储结构
typedef struct SequentialString{
char ch[MAXSIZE]; // 设定ch[0]不存放值,方便KMP
int length; // 串的实际长度,不包括'\0'
}SqString;
// 初始化串
void StrAssign(SqString &S, char chars[]) {
int i;
for(i = 0; chars[i] != '\0'; 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<T,返回值<0;
int StrCompare(SqString S, SqString T) {
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;
}
/*
总结:
1. i=0和i=1的区别,主要在变量比较/终止条件判断,不涉及变量相减的部分。
例:
int i = 0, j = 0;
while (i < S.length && j < T.length)
(j >= T.length)
-->
int i = 1, j = 1;
while (i <= S.length && j <= T.length)
(j > T.length)
2. for(int i = 0; chars[i] != '\0'; i++)
在Dev C++中无法识别这种定义方式,在for语句中定义的元素在此循环外无法使用
3. SqString第一位存不存字符的区别:
第一位存字符:i = i-j+1;
第一位不存字符i = i-j+2;
4. 打印未知长度int型数组
int array[] = {1,2,3,4,45};
cout << sizeof(array)/sizeof(int);
return 0;
*/
运行结果: