串的顺序存储,基本操作,及KMP模式匹配算法实现

数据结构 DATA STRUCTURE

四、串

4.1 串的顺序存储结构实现

  1. 顺序存储

    // 串的顺序存储 
    typedef struct SequentialString{
          
          
    	char ch[MAXSIZE];   // 设定ch[0]不存放值,方便KMP算法 
    	int length;			// 串的实际长度,不包括'\0' 
    }SqString;
    
  2. 链式存储

    // 串的链式存储
    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() 图解:
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];
	}
} 

参考资料:

  1. 凡三岁爱学习. KMP算法之求next数组代码讲解_哔哩哔哩_bilibili(我称之为b站最通透讲解)
  2. 王道论坛. 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

调试截图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

参考资料:

  1. 关于DevC++如何调试的问题,还不会调试的同学看这里—>>>超级详细调试教程,手把手教你如何调试_落雨归林的博客-CSDN博客_devc++调试
  2. dev c++怎么调试查看数组变量 - CSDN

全部代码:(可直接运行)

代码:

#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;
*/

运行结果:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_45770232/article/details/124707486