一.串的基本概念
1.串,也称字符串,是由零个或多个字符组成的有限序列,记作S= “a1,a2,a3,…ai,…an”;其中S是串名,双引号括起来的有限序列是串值,a;(1≤i≤n)可以是任意字符(如字母、数字或其他字符),串长是串中所包含的字符个数。
2.空串是指长度为零的串,空格串是指构成串的所有字符都是空格,例如“”,“ ”都 是空格串。
注意:空串的长度一定等于0,而空格串的长度一定大于0。
3.串相等是指两个串的串值相等,即要求两个串的长度相等,且各个对应位置的字符也都相同。 例如,串A=“abe",串B=“abe" ,串C=“abcd",串D=“bae" ,串A等于串B.但串A与串C和串D均不相等。
4.子串是指串中任意个连续字符组成的子序列,包含子串的串相应地称为主串。子串在主串中首次出现时,该子串的首字符对应在主串中的序号称为子串在主串中的序号(或位置)。
二.串的存储结构
1.定长顺序串
定长顺序串是串顺序存储结构的一种,顺序存储结构意味着它所占的内存空间是一块连续的,而定长意味着空间大小在串创建之初就确定了,无法动态分配内存,如果有超过的部分只能舍去。在这里我们使用尾部加“ \0 ”的方法来表示串结束,因此实际要开辟的空间是 MAXSTRLEN + 1。
2.堆串
存储方法:将一个地址连续,容量很大的存储空间作为字符串的可用空间,执行程序是动态分配
串名符号表:所有串的存储映像构成一个符号表。借助此结构可以在串名与串值之间建立一个对应关系,称为串名的存储映像
#include <iostream>
#include <stdlib.h>
#include <string.h>
using namespace std;
int main()
{
char* a1;
char* a2;
a1 = (char*)malloc(10 * sizeof(char));
strcpy_s(a1,10, "qwertyuio");//将字符串"qwertyuiop"复制给a1
a2 = (char*)malloc(10 * sizeof(char));
strcpy_s(a2, 10,"asdfghjkl");
int lengthA1 = strlen(a1);//a1串的长度
int lengthA2 = strlen(a2);//a2串的长度
//尝试将合并的串存储在 a1 中,如果 a1 空间不够,则用realloc动态申请
if (lengthA1 < lengthA1 + lengthA2)
{
a1 = (char*)realloc(a1, (lengthA1 + lengthA2 + 1) * sizeof(char));
}
//合并两个串到 a1 中
for (int i = lengthA1; i < lengthA1 + lengthA2; i++)
{
a1[i] = a2[i - lengthA1];
}
//串的末尾要添加 \0,避免出错
a1[lengthA1 + lengthA2] = '\0';
cout << a1 <<endl;
//用完动态数组要立即释放
free(a1);
free(a2);
return 0;
}
3.块链串
串的块链存储,指的是使用链表结构存储字符串。类似于链表,这种存储结构比定长顺序串和堆串的顺序存储的插入,删除等操作更方便,更加适用;
三.串的模式匹配
模式匹配是在主串中查找模式子串的操作,即求子串位置,这是文本处理中最常用、最重要的操作之一,例如word中的查找功能。<string.h>库文件中虽然提供了字符定位函数strchr,但是没有提供模式匹配函数。模式匹配的算法有很多,这里介绍两种经典的模式匹配算法- Brute Force算法和KMP算法。
3.1Brute Force算法(暴力算法)
Brute Force算法是普通的模式匹配算法。将主串S的第1个字符和模式串T的第1个字符比较,若相等,继续逐个比较后续字符;若不等,从主串的下一字符起,重新与模式串的第二个字符比较,直到主串的一一个连续 子串字符序列与模式相等,返回值为S中与T匹配的子序列第一个字符的序号,即匹配成功;否则,匹配失败,返回值0。
时间复杂度分析:
假设主串S长度为n,子串T长度为m,最好的情况是第一次 比较就匹配成功,时间复杂度为O(m);最坏的情况是到最后一次比较才匹配成功,则时间复杂度为0((n-m+1) * m),可见这种算法的效率很低。(主要是回溯次数过多,增加了检验的次数,自然也增加了检查的的时间。)
小技巧:0((n-m+1) * m):可以理解是除去m个字串后的n还要和字串的m个字符一遍遍比过,最后到了最后一个,从n-m+1的地方开始比,发现可以比对成功;
#include <iostream>
#include <stdlib.h>
using namespace std;
#define maxsize 256
typedef struct
{
char ch[maxsize]; //字符数组
int length; //串长
}SqString;
int PatternString(SqString* S, SqString* T)
{
int i, j;
i = 0; //初始化主串开始匹配长度
j = 0; //子串开始匹配长度
while (i<S->length && j<T->length)
{
if (S->ch[i] == T->ch[j])
{
i++;
j++;
}
else
{
i = i - j + 1; //本次没有找到,主串回溯到原位置的下一个位置,
j = 0; // 重新从子串的第一个位置开始匹配
}
}
if (j >= T->length) //最终匹配成功,返回首字母的位置
{
return i - T->length + 1;
}
else
{
return -1; //没有匹配成功,返回-1
}
}
int main()
{
SqString* S, * T; //主串、子串
int j = -1;
S = (SqString*)malloc(sizeof(SqString));
T = (SqString*)malloc(sizeof(SqString));
cout << "输入主串:" << endl;
gets_s(S->ch);
cout << "输入子串" << endl;
gets_s(T->ch);
S->length = strlen(S->ch); //获取主串长度
T->length = strlen(T->ch); //获取子串长度
j = PatternString(S,T); //模式匹配,算法核心
if(j>=0)
{
cout << "匹配成功! 匹配的位置为:" << j << endl;
}
else
{
cout << "没有找到该字符串" << j << endl;
}
return 0;
}
3.2 KMP算法
通过前面介绍的简单字符串匹配算法时间复杂度发现.此算法低效的原因是因为不断的回溯游标,并逐位进行比较.要想提高效率就必须减少游标回溯。于是D. E. Knuth、J.H.Morris和V.R.Pratt同时提出了模式匹配的改进算法KMP算法。
KMP算法的核心思想是避免不必要的回溯,主串游标不进行回溯,子串游标在每个位置的回溯位数由当前位置之前字符与自身开始几位的字符匹配程度来决定。所以实现KMP模式匹配的关键,是推导出待匹配的字符串(子串)各个位置应该回溯到的位置,我们把各个位置放到一个next数组中。下面通过一个示例来说明next[ ]数组的推导。
如果主串为S=”abcababcabe“,待匹配子串为T=“abcabe”,j为串T的游标,按照如下步骤推导:
(1)当j=0时,前面没有字符,next[0]=-1,即回溯结束;
(2)当j=1时,前面只有一个字符a,则next[1]=0;
(3)当j=2时,前面有两个字符a和b,并且a和b不相等,则next[2]=0;
(4)当j=3时,前面字符串abc,无相等字符,则next[3]=0;
(5)当j=4时,前面字符abca,第1位字符与第4位相等,则next[4]=1;
(6)当j=5时,前面字符abcab,第4、5位字符相等,第1、2位字符相等,则next[4]=2;最后推导出数组next={一1,0,0,0,1,2},next数组存放了子串T各字符在匹配时回溯的位置,可以减少很多不必要的回溯。(这是一个大体思路)
小提示:KMP教学,我是看这个老师学会的,但是让我用文字很难形容,大家可以去看看;
下面是参考代码;(KMP最难理解的就是用数组记录下标,减少回溯)
#include <iostream>
#include <stdlib.h>
using namespace std;
#define maxsize 256
typedef struct
{
char ch[maxsize]; //字符数组
int length; //串长
}SqString;
void GetNext(SqString* T , int next[])
{
next[0] = -1; //初始化next[0]值为-1
int i = 0;
int j = -1;
while (i < T->length - 1) //遍历所有字符
{//依次比较前面的字符,如有相等的字符则记录在next数组中
if (j == -1 || T->ch[i] == T->ch[j])
{
i++;
j++;
next[i] = j;
}
else
{
j = next[j];
}
}
}
int PatternString(SqString* S, SqString* T)
{
int i, j;
int next[20];
GetNext(T, next); //获取next数组
i = 0; //初始化主串开始匹配位置
j = 0; //子串开始匹配配置
while (i < S->length && j < T->length)
{
if (j == -1 || S->ch[i] == T->ch[j])
{
i++;
j++;
}
else
{
j = next[j]; //本次没有匹配到,字串回溯到next推导的位置上
}
}
if (j >= T->length) //最终匹配成功,返回子串的首字母位置
{
return i - T->length + 1;
}
else
{
return -1; //没有匹配成功,返回-1
}
}
int main()
{
SqString* S, * T; //主串、子串
int j = -1;
S = (SqString*)malloc(sizeof(SqString));
T = (SqString*)malloc(sizeof(SqString));
cout << "输入主串:" << endl;
gets_s(S->ch);
cout << "输入子串" << endl;
gets_s(T->ch);
S->length = strlen(S->ch); //获取主串长度
T->length = strlen(T->ch); //获取子串长度
j = PatternString(S,T); //模式匹配,算法核心
if(j>=0)
{
cout << "匹配成功! 匹配的位置为:" << j << endl;
}
else
{
cout << "没有找到该字符串" << j << endl;
}
return 0;
}
四.例题
医学研究者最近发现了某新病毒,通过对这些病毒的分析,得知他们的DNA序列都是环状的。现在研究者已收集了大量的病毒DNA和人的DNA数据,想快速检测出这些人是否感染了相应的病毒。为了研究方便,研究者将人的DNA和病毒DNA均表示成由一些字母组成的字符串序列,然后检测眸中病毒DNA序列是否在患者的DNA序列中出现过,如果出现过,则此人感染了该病毒,否则没有感染。例如,假设病毒的DNA序列为baa,患者1的DNA序列为aaabbba,则感染;患者2的DNA序列为babbba,则未感染。(注意,恩德DNA序列式线性的,而病毒的DNA序列为环状的)
(算法采用KMP算法,与上方代码基本相同)
//**KMP算法解析**
#include<iostream>
#include<cstring>
#include<string>
#include<fstream>
using namespace std;
void get_next(string str2, int* next)
{
int i, j;
i = 1;
j = 0;
next[1] = 0;
while (i < str2.size())
{
if (j == 0 || str2[i] == str2[j])
{
++i;
++j;
if (str2[i] != str2[j])
{
next[i] = j;
}
else
{
next[i] = next[j];
}
}
else
{
j = next[j];
}
}
}
int kmp(string & str1, string & str2, int pos)
{
int i = pos;
int j = 0;
int next[255];
get_next(str2, next);
int len1 = str1.size();
int len2 = str2.size();
while (i < len1 && j < len2)
{
if (j == 0 || str1[i] == str2[j])
{
i++;
j++;
}
else
{
j = next[j];
}
}
if (j >= len2)
{
return i - len2;
}
else
{
return -1;
}
}
int main()
{
int pos = 0;
string str1;
string str2;
while (1)
{
cout << "请输入患者DNA" << endl;
cin >> str1;
cout << "请输入病毒DNA" << endl;
cin >> str2;
if (str1 == "0" && str2 == "0")
{
break;
}
int result = kmp(str1, str2, pos);
if (result != -1)
{
cout << "YES ! 您已患病" << endl;
cout << endl;
}
else
{
cout << "NO !恭喜你身体健康" << endl;
cout << endl;
}
}
return 0;
}