相关概念
- 串:又称字符串,是由零个或多个字符组成的有限序列。字符串通常用双引号括起来,例如S=“abcdef”,S为字符串的名字,双引号里面的内容为字符串的值。
- 串长:串中字符的个数,例如S串长为6
- 空串:0个字符的串,串长为0
- 子串:串中任意个连续的字符组成的子序列,称为该串的子串,原串称为子串的主串。例如T=“cde”,T是S的子串。子串在主串中的位置,用子串的第一个字符在主串中出现的位置表示。T在S中的位置为3
- 空格:也算一个字符,例如X="abc fg" X的串长为6
- 空格串:全部由空格组成的串称为空格串,空格串不是空串
顺序存储
顺序存储是用一段连续的空间存储字符串。可以预先分配一个固定长度Maxsize的空间,在这个空间中存储字符串。
三种方式
- 用'\0'表示字符串结束,'\0'不算在字符串长度内。这样做有一个问题:如果想知道串的长度,需要从头到尾遍历一遍,如果经常需要用到串的长度,每次遍历一遍复杂性较高,因此可以考虑将字符串的长度存储起来以便使用。
- 在0空间存储字符串的长度
- 结构体变量存储字符串长度(静态分配,容易超过最大长度,出现溢出)
链式存储
单链表存储字符串时,虽然插入和删除非常容易,但是这样做也有一个问题:一个节点只存储一个字符,如果需要存储的字符特别多,会浪费很多空间。因此也可以考虑一个节点存储多个字符的形式,例如一个节点存储3个字符,最后一个节点不够3个时用#代替,如图
但是这样做也有一个大问题:如在第2个字符之前插入一个元素,就需要将b和c后移,那么这种后移还要跨到第二个节点,如同“蝴蝶效应”,一直波及最后一个节点,麻烦就大了!
因此字符串很少使用链式存储结构,还是使用顺序存储结构更灵活一些。
模式匹配BF算法
模式匹配:子串的定位运算称为串的模式匹配或串匹配。
假设有两个串S,T,设S为主串,T为子串,也称模式。在主串S中查找与模式T相匹配的子串,如果查找成功,返回匹配的子串第一个字符在主串中的位置
最笨的办法就是穷举所有S的所有子串,判断是否与T匹配,该算法称为BF算法
因为串的模式匹配没有插入、合并等操作,不会发生溢出,因此可以采用第2种字符串顺序存储方法,用0空间存储字符串长度。例如,T的顺序存储方式如图
复杂度分析:
#include <iostream>
#include <cstring>
using namespace std;
#define Maxsize 100
typedef char SString[Maxsize+1];
bool StrAssign(SString &T,char *chars){//生成一个其值等于chars的串T
int i;
if(strlen(chars)>Maxsize)return false;
else {
T[0]=strlen(chars);
for(int i=1;i<=T[0];i++){
T[i]=*(chars+i-1);
cout <<T[i]<<" ";
}
cout <<endl;
cout <<"length="<<int(T[0])<<endl;
return true;
}
}
int Index_BF(SString S,SString T,int pos){//BF算法
// 求子串T在主串S中第pos个字符之后第一次出现的位置
//其中,T非空,1≤pos≤s[0],s[0]存放S串的长度
int i=pos,j=1,sum=0;
while(i<=S[0]&&j<=T[0]){
sum++;
if(S[i]==T[j]){//如果相等,则继续比较后面的字符
i++;j++;
}else{
i=i-j+2;//i回退到上一轮开始比较下一个字符
j=1;//j回退到第一个字符
}
}
cout <<"比较的次数是:"<<sum<<endl;
if(j>T[0])return i-T[0];
else return 0;
}
int main(){
SString S,T;
char str[100];
cout <<"串S:"<<endl;
cin >>str;
StrAssign(S,str);
cout<<"串T:"<<" ";
cin>>str;//aaaab
StrAssign(T,str);
cout <<Index_BF(S,T,1);
}
模式匹配KMP算法
BF的改进
直接移动模式串,使前缀移到了后缀的位置
其实i不用回退,让j回退到第3个位置,接着比较即可,如图
因为T串中开头的两个字符和i指向的字符前面的两个字符一模一样,如图。这样j就可以回退到第3个位置继续比较了,因为前面两个字符已经相等了
假设T中当前j指向的字符前面的所有字符为T',只需要比较T′的前缀和T'的后缀即可,如图
前缀是从前向后取若干个字符,后缀是从后向前取若干个字符。注意:前缀和后缀不可以取字符串本身。如果串的长度为n,前缀和后缀长度最多达到n-1
动态规划
那么next[j+1]=?
分为以下两种情况:
- t[k]=t[j],那么next[j+1]=k+1,即相等前缀和后缀的长度比next[j]多1
- 如果t[k]与t[j]不相等,则回退向前找;找到,相等,则next[j+1]=k'+1;都不相等,直到找到next[1]=0停止
void get_next(SString T, int next[]) //求模式串T的next函数值
{
int j=1,k=0;
next[1]=0;
while(j<T[0]) // T[0]为模式串T的长度
if(k==0||T[j]==T[k])
next[++j]=++k;
else
k=next[k];
}
int Index_KMP(SString S, SString T, int pos, int next[])
{ //利用模式串T的next函数求T在主串S中第pos个字符之后的位置
//其中,T非空,1\leqslantpos\leqslantS[0],S[0]为模式串S的长度
int i=pos,j=1;
while(i<=S[0]&&j<=T[0])
{
if(j==0||S[i]==T[j]) // 继续比较后面的字符
{
i++;
j++;
}
else
j=next[j]; // 模式串向右移动
}
if(j>T[0]) // 匹配成功
return i-T[0];
else
return 0;
}
改进的KMP算法
void get_next2(SString T, int next[]) //求模式串T的next函数值
{
int j=1,k=0;
next[1]=0;
while(j<T[0]) // T[0]模式串T的长度
{
if(k==0||T[j]==T[k])
{
j++;
k++;
if(T[j]==T[k])
next[j]=next[k];
else
next[j]=k;
}
else
k=next[k];
}
字符串的应用——病毒检测
题目:疫情暴发,专家发现了一种新型环状病毒,这种病毒的DNA序列是环状的,而人类的DNA序列是线性的。专家把人类和病毒的DNA表示为字母组成的字符串序列,如果在某个患者的DNA中发现这种环状病毒,说明该患者已被感染病毒,否则没有感染。
例如:病毒的DNA为“aabb”,患者的DNA为“ecab”,说明该患者已被感染。因为病毒是环状的,因此“abba”也是该病毒序列,它在患者的DNA中出现了。
解题思路:
该问题属于字符串的模式匹配问题,可以使用前面讲的BF或KMP算法求解。这里需要对环状病毒进行处理,然后调用模式匹配算法即可
环形处理
1)从0下标取4个字符:aabb
2)从1下标取4个字符:abba
3)从2下标取4个字符:bbaa
4)从3下标取4个字符:baab
这4个序列都是病毒序列的变种
线性处理
将该病毒序列扩大两倍,如图。从每个下标(1、2、3、4)开始取4个字符,分别为aabb、abba、bbaa、baab,这4个序列都是病毒序列的变种
运算
依次把每一个环状病毒变种作为子串,把患者DNA序列作为主串,进行模式匹配。一旦匹配成功,立即结束,返回已感染病毒。
bool Virus_detection(SString S, SString T)//病毒检测
{
int i,j;
SString temp;//temp记录病毒变种
for(i=T[0]+1,j=1;j<=T[0];i++,j++)//将T串扩大一倍,T[0]为病毒长度
T[i]=T[j];
for(i=0;i<T[0];i++)//依次检测T[0]个病毒变种
{
temp[0]=T[0];病毒变种长度为T[0]
for(j=1;j<=T[0];j++)//取出一个病毒变种
temp[j]=T[i+j];
if(Index_KMP(S,temp,1))//检测到病毒
return 1;
}
return 0;
}