详解数据结构:字符串

一、字符串

串:又称字符串,是由零个或多个字符组成的有限序列。

字符串通常用双引号括起来,例如S=”abcdefg”,S为字符串的名字,双引号里面的内容为字符串的值。

串长:串中字符的个数,例如S的串长为6。

空串:零个字符的串,串长为0。

子串:串中任意个连续的字符串组成的子序列,称为该串的子串,原串称为子串的主串。子串在主串中的位置,用子串的第一个字符串在主串中出现的位置表示。

注意:空格也算一个字符。

空格串:全部由空格组成的串为空格串。

注意:空格串不是空串。

字符串的存储可以使用顺序存储和链式存储两种方式。

1、字符串的顺序存储

顺序春初使用一段连续的空间存储字符串。可以预先分配一个固定长度Maxsize的空间,在这个空间中存储字符串。

顺序存储又有三种方式:

(1)以’\0’表示字符串结束

在C、C++、Java语言中,通常用’\0’表示字符串结束,’\0’不算在字符串长度内。

这样做有一个问题,如果想知道串的长度,需要从头到尾遍历一遍,如果经常需要用到串的长度,每次遍历一遍复杂性较高,因此可以考虑将字符串的长度存储起来方便使用。

(2)在0空间存储字符串长度

下标为0的空间不使用,因此可以预先分配Maxsize+1的空间,在下标为0的空间中存储字符串长度。

(3)结构体变量存储字符串的长度。

除上述方法之外,也可以将字符串长度存储在结构体中。

typedef struct{

    char ch[Maxsize];//字符型数组

    int length;//字符串的长度

}SString;

这样做也有一个问题,串的运算如和并、插入、替换等操作,容易超过最大长度,出现溢出,可以采用动态分配空间的方法,其结构体定义如下:

typedef struct{

    char   *ch;//指向字符串指针

    int length;//字符串的长度

}SString;

2、字符串的链式存储

和顺序表一样,顺序存储的串在插入和删除操作时,需要移动大量元素,因此也可以采用链表的形式存储。

单链表存储字符串时,虽然插入和删除非常容易,但是这样做也有一个问题:一个节点只存储一个字符,如果需要存储的字符特别多,会浪费很多空间,因此也可以考虑一个节点存储多个字符的形式。

但是这样做也有一个大问题:如在第2个字符之前插入一个元素,就需要将b和c后移,那么这种后移还要跨过第二个节点,如“蝴蝶效应”,一直波及到最后一个节点。

因此字符串很少使用链式存储结构,还是使用顺序存储结构更灵活一些。

二、模式匹配BF算法

匹配模式:字串的定位运算称为串的模式匹配或串匹配。

假设两个串S、T,设S为主串,也称为正文串;T为字串,也称为模式。主串S查找与模式T匹配的子串,如果查找成功,返回匹配的子串第一个字符在主串中的位置。

最笨的方法就是穷举所有S的所有子串,判短是否与T匹配,该算法称为BF算法。

算法步骤:

1.从S第一个字符开始,与T第一个字符比较,如果相等,继续比较下一个字符,否则则转向下一步。

2.从S第二个字符开始,与T第一个字符比较,如果相等,继续比较下一个字符,否则转向下一步。

3.从S第三个字符开始,与T第一个字符比较,如果相等,继续比较下一个字符,否则转向下一步。

……

4.如果T比较完毕,则返回T在S中第一个字符出现的位置;

5.如果S比较完毕,则返回0,说明T在S中未出现。

完美图解:

例如:S=’abaabaabeca’,T=’abaabe’,求子串T在主串S中的位置。

(1)从S第一个字符开始:i=1,j=1,比较两个字符是否相等,如果相等,则i++,j++;如果不等,则转向下一步,如下图。

(2)i退回到i-j+2的位置,j回退到1的位置,即i-j+2=6-6+2,即i从S第2个字符开始,j从T的第1个字符开始。比较两个字符是否相等,如果相等,则i++,j++;如果不等,则转向下一步,如下图。

解释:为什么i要退回i-j+2的位置呢?如果本趟开始的位置为a,那么下一趟开始的位置就是a的下一个字符b的位置,这个位置正好师i-j+2。

(3)i回退到i-j+2的位置,i=2-1+2=3,即从S第3个字符开始,j=1,比较两个字符是否相等,如果相等,则i++,j++;如果不等,则转向下一步。

(4)i回退到i-j+2的位置i=4-2+2=4,即从S第4个字符开始,j=1,比较两个字符是否相等,如果相等,则i++,j++;如果不等,则转向下一步。

(5)T比较 完毕,返回子串T在主串S中第一个字符出现的位置,即i-m=10-6,m为T的长度。

因为串的匹配模式没有插入、合并等操作,不会发生溢出,因此可以采用第二种字符串顺序存储方法,用0空间存储字符串长度。

代码实现:

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[i])//如果相等,则比较后面的字符串

        {

           i++;

           j++;

        }

        else

        {

           i=i-j+2;//i回退到上一轮开始比较的下一个字符

           j=1;//j回退到第1个字符

        }

    }

    count<<"一共比较了"<<sum<<"次"<<endl;

    if(j>T[0])//匹配成功

    {

        return i-T[0];//i减去T的长度

    }

    else

    {

        return 0;

    }

}

算法复杂度分析:

设S、T串的长度分别为n、m,则BF算法的时间复杂度分为以下两种情况:

(1)最好情况

在最好的情况,每一次匹配都在第一次比较发现不等。

假设第i次匹配成功,则前i-1次匹配都进行了1次比较,一共i-1次,第i次匹配成功时进行了m次比较,则总的比较次数时i-1+m。在匹配成功的情况下,最多需要n-m+1次匹配,即模式串正好在主串的最后端。假设每一次匹配成功的概率均等,概率pi=1/(n-m+1),则在最好的情况下,匹配成功的平均次数为:

最好情况的平均复杂度为O(n+m)。

(2)最坏情况

在最坏情况下,每一次匹配都比较到T的最后一个字符发现不等,回退重新开始,这样每次匹配都需要比较m次。

假设第i次匹配成功,则前i-1次匹配都进行了m次比较,第i次匹配成功时也进行了m次比较,则总的比较次数为i×m。在匹配成功的情况,最多需要n-m+1次匹配,即模式串正好在主串的最后端。假设每次匹配成功的概率均等,概率pi=1/(n-m+1),在最坏的情况下,匹配成功的平均次数是:

最坏的情况下的平均时间复杂度是O(n×m)。

三、模式匹配KMP算法

实际上,完全没必要从S的每一个字符开始穷举每一种情况,对该算法进行了改进,提出了KMP算法。

从S第一个字符开始:i=1,j=1,比较两个字符是否相等,如果相等,则i++,j++;第一次匹配不相等。

按照BF算法,如果不等,则i回退到i-j+2,j回退到1,即i=2,j=1。

其实i不用回退,让j回退到第3个位置,接着比较即可。

是不是像T向右滑动了一段距离?为什么呢?

因为T串中开头的两个字符和i指向的字符前两个字符一模一样。

这样j就可以回退到第3个位置继续比较了,因为前面两个字符已经相等了。

那怎么知道T开头的两个字符和i指向的字符前面两个字符一模一样?难道还要比较?我们发现i指向的字符前面的两个字符和T中j指向的字符前面两个字符一模一样,因为他们一直相等,i++、j++才会走到当前的位置。

也就是说,我们不必判断开头的两个字母和i指向的字符前面,只需要在T本身比较就可以了。假设T中当前j指向的字符前面所有字符与为T’,只需要比较T’的前缀和T’的后缀即可。

前缀是从前向后取若干个字符,后缀是从后先前取若干个字符。注意:前缀和后最不可以取字符串本身。如果串的长度为n,前缀和后缀的长度最多达到n-1。

判断T’=”abaab”的前缀和后缀是否相等,并找出相等前缀后缀的最大长度。

  1. 长度为1:前缀’a’,后缀’b’,不等。
  2. 长度为2:前缀为’ab’,后缀为’ab’,相等。
  3. 长度为3,前缀为’aba’,后缀为“aab”,不等。
  4. 长度为4,前缀为’abaa’,后缀为“baab”,不等。

相等前缀后缀的最大长度为l=2,则j就可以回退到l+1=3个位置继续比较了。因此,当i、j指向的字符不等时,只需要求出T’的相同前缀后缀的最大长度为l,i不变j回退到l+1的位置继续比较即可。

现在可以写出通用公式,next[j]表示j需要回退的位置,T=’t1t2…tj-1’,则:

根据公式很容易求出T=’abaabe’的next[]数组,如下图:

解释如下:

  1. j=1:根据公式next[1]=0
  2. j=2:T’=’a’,没有前缀后缀,next[2]=1。
  3. j=3:T’=’ab’,前缀后缀不等next[3]=1。
  4. j=4:T’=’aba’,前缀为’a’,后缀为’a’,相等且l=1:前缀为“ab”,后缀为“ba”,不等;因此next[4]=l+1=2。
  5. j=5:T’=’abaa’,前缀为’a’,后缀为’a’,后缀为’a’,相等且l=1;前缀为’ab’,后缀为‘aa’,不等;前缀为’aba’,后缀为’baa’,不等因此next[5]=l+1=2。
  6. j=5:T’=’abaab’,前缀为’a’,后缀为’a’,后缀为’a’,相等且l=1;前缀为’ab’,后缀为‘ab’, 相等且l=2;前缀为’ abaa’,后缀为’ baab’,不等因此next[6]=l+1=3。

这样找所有的前缀和后缀比较,是不是也是暴力穷举?那怎么办呢?

可以用动态规划递推。

首先大胆假设,我们已经知道了next[j]=k,T’=’t1t2…tj-1’,那么T’的相等前缀、后缀最大长度为k-1。

那么next[j+1]=?

考查以下两种情况。

(1)tk=tj,那么next[j+1]=k+1,即相等前缀和后缀的长度比next[j]多1。

(2)tk≠tj,但两者不相等时,我们又开始了这两个串的模式匹配,回退找next[k]=k’的位置,比较tk’与tj是否相等。

如果tk’=tj相等,则next[j+1]=k’+1。

如果tk’不等于tj,则继续回退找next[k’]=k’’,比较tk’’与tj是否相等。

一直循环,直到找到next[1]=0停止。

代码实现:

求解next[]的代码实现如下:

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];

        }

    }

}

有了next[]数组,就很容易进行模式匹配了,当S[i]≠T[j]时,i不动,j回退到next[j]的位置继续比较即可。

代码实现:

KMP算法的代码实现如下:

void Index_KMP(SString S,SString T,int pos,int next[]//求模式串T的next函数值

{

    //利用模式串T的next函数求T在主串s中第pos个字符之后的位置

    //其中,T非空,1≤pos≤s[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;

    }

}

算法复杂度分析

设S、T串的长度分别为n、m。KMP的算法特点时:i不回退,当S[i]≠T[j]时,j回退到next[j],重新开始比较。最坏的情况下扫描整个S串,其时间复杂度为O(n)。计算next[]数组需要扫描整个T串,其时间复杂度为O(m),因此总的时间复杂度为O(n+m)。

需要注意的时,尽管BF算法最坏情况下时间复杂度为O(n×m),KMP算法的时间复杂度为O(n+m)。但是实际运用中,BF的时间复杂度一般为O(n+m),因此仍然有很多地方使用BF算法进行模式匹配。只有在主串和主串有很多部分匹配的情况下,KMP才显得更优越。

四、改进的KMP算法

在KMP算法中,next[]求解非常方便、迅速,但是也有一个问题,当si≠tj时,j回退到netx[j](k=next[j]),然后si和tk比较。这样确实没错,但如果tk=tj,这次比较就没必要了,因为刚才就是因为si≠tj才hui'tui的,那么肯定si≠tk,完全没必要再比了,如下图:

再向前回退,找到下一个位置next[k],继续比较就可以了,当si≠tj时,本来应该j回退到netx[j](k=next[j]),sj与tk比较。但是如果tk=tj,则不需要比较了,继续回退到下一个位置next[k],减少了一次无效比较。

修改程序:

求解next[]的改进代码实现如下:

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])

        {

           j++;

           k++

           if(T[j]==T[k])

           {

               next[j]=next[k];

           }

           else

           {

               next[j]=k

           }

        }

        else

        {

           k=next[k];

        }

    }

}

算法复杂度分析

设S、T的长度为n、m,改进的KMP算法只是在求解next[]从常数上的改进,并没有降解,因此算法复杂度依然时O(n+m)。

五、字符串的应用——病毒检测

题目:疫情爆发,专家发现发现一种新型环状病毒,这种病毒的DNA序列是环状的,而人类的DNA序列是线性的。专家把人类和病毒的DNA表示为字母组成的字符串序列,如果在某个患者的DNA中发现这种环状病毒,说明该患者已经被感染病毒,否则没有感染。

解题思路:

该问题属于字符串的模式匹配问题,可以用前面讲的BF或KMP算法求解,这里需要对环状病毒进行处理,然后调用模式匹配算法即可。

如何处理环状病毒呢?

1、环形处理

使用循环存储的方式,类似循环队列或循环链表的处理方式。假设病毒的DNA长度为m,依次从环状存储空间中每一个下标开始,取m个字符作为病毒序列,

例如,病毒序列为aabb,如下图所示。从每个下标开始取4个字符。

  1. 从0下标取4个字符:aabb。
  2. 从1下标取4个字符:abba。
  3. 从2下标取4个字符:bbaa。
  4. 从3下标取4个字符:baab。

这4个序列都是病毒序列的变种。

2、线性处理

将病毒序列扩大两倍,依次从每个下标开始,取m个字符,作为病毒序列。例如:病毒序列:aabb。将病毒序列扩大两倍。从每个下标(1、2、3、4)开始取4个字符,分别为aabb、abba、bbaa、baab,这4个序列都是病毒序列的变种。

算法步骤:

  1. 首先对环状病毒进行处理(环形处理或线性处理)
  2. 依次把每一个环状病毒变种作为子串,把患者DNA序列作为主串,进行模式匹配。一旦匹配成功,立即结束,返回已感染病毒。
  3. 重复运行第二步。
  4. 如果检验所有病毒变种都为匹配成功,返回未感染病毒。

完美图解:

例如:患者的DNA序列为eabbacab,病毒DNA序列为aabb,检验患者是否感染病毒。

  1. 首先采用线性处理,将病毒序列扩大两倍。

  1. 从下标1开始取4个字符,为aabb,与患者的DNA序列ecabbacab进行模式匹配,未匹配成功。
  2. 从下表2开始取4个字符,为abba,与患者的DNA序列eabacab进行模式匹配,匹配成功:eabbacab,返回该患者已感染该病毒

代码实现:

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;//未检测到病毒

}

算法复杂度分析:假设病毒DNA序列长度为m,则一共有m个变种,需要进行m次模式匹配,每次模式匹配如果使用KMP算法,其时间复杂度为O(n+m),则种的时间复杂度是O(m×(n+m))。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sɪʟᴇɴᴛ໊ོ5329

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值