串(大话数据结构略读)

目录

串的定义

串的比较

串的抽象数据类型 

串的存储结构

 串的顺序存储结构

串的链式存储结构 

朴素的模式匹配算法        

匹配算法内容 

算法质量分析 

KMP模式匹配算法       

概念引入

next数组值推导

KMP模式算法实现

计算子串next数组的代码

正式开始与主串匹配的代码

kmp模式匹配算法小结 

KMP模式匹配算法改进

  


 

串的定义

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

串的比较

 字符的编码

        计算机的常用字符是使用标准的ASCLL编码,更准确一点,即是7位二进制数表示一个字符,总共可表示128个字符,之后扩展ASCLL编码由8位二进制数表示一个字符,总共可以表示256个字符,此时可以满足以英语为主的语言和特殊符号的输入输出。

        对于我们国家的汉字而言,256个字符显然是不够的,于是便有了由以十六进制为一个码点Unicode编码,为了和ASCLL编码兼容,其前256个字符与ASCLL码完全相同。Unicode编码发展了不同的版本,可以粗略了解这篇文章unicode编码略读

串的比较        

        若我们要在C语言中比较两个串是否相等,必须是其串的长度以及它们各个位置的字符都相等时,才算是相等。

        1. 例如s= “happy”,t=“happ”,s>t,因为t比s多了一个字母。

        2. 例如s=“happen”,t=“happy”,虽说s比t多两个字母,但因为e的ascll码为101,而y的ascll码为121,所以s<t。

         

串的抽象数据类型 

        串的基本操作与线性表有较大差别。线性表更关注的是单个元素的操作,比如查找一个元素;而串更多的是查找子串位置、得到指定位置的子串等操作。

        而对于不同的高级语言,对串的基本操作会有不同的定义方法,比如C#中就有Tolower转小写等较为方便的操作,它们其实就是前面这些基本操作的扩展函数。

串的存储结构

 串的顺序存储结构

        串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。

        

        注意字符串的操作,如两串的相连接concat,新串的插入strinsert,都有可能使串序列长度超过了数组长度。

        对于串的顺序存储,有一些变化,串值的存储空间可在程序执行过程中动态分配而得。不如在计算机中存在一个自由存储区,叫做“堆”。这个堆可由C语言的动态分配函数malloc和free来管理。

串的链式存储结构 

        与线性表大致相似,但因串结构的特殊性,每个数据元素是一个字符,如果也简单的应用链表存储串值,一个结点对应一个字符,会造成很大的空间浪费。

        因此,一个结点需考虑存放多个字符,最后一个结点若是未被占满,可用其他非串值字符补全,如“#”。

         

         这里一个结点存多少个字符才合适变得很重要,这会直接影响着串处理的效率,需根据实际情况作出选择。但串的链式结构除了连接串与串操作有一定方便之外,总的来说不如顺序存储结构灵活。

朴素的模式匹配算法        

        若要找一个单词在一篇文章中的定义问题。这种子串的定位操作通常称作串的模式匹配。

        假如我们要从下面主串S=“goodgoogle”中,找到T=“google”这个子串的位置。我们通常需要下面的步骤。

        注意:串可以将数组首元素a[0]中放置字符长度如下:

匹配算法内容 

代码如下 :

        

图解初次验证子串是否与主串相等

当不匹配时,指针回归

返回位置判断 

          而if (j >T[0])的作用是在匹配过程中判断是否已经匹配完了子串T的所有字符。如果匹配完了,则返回子串T在主串s中第pos个字符之后的位置;

        因为是算法使然,字符匹配时,每一次循环后都会使i,j向后递增一位,所以完全匹配时,实际当前循环也会使i,j向后推进一位。

        此处有一误点:以为到达上面效果的原因是++j;无论是j++还是++j,都会使j的值增加1。所以此处是算法使然。

算法质量分析 

        分析一下,最好的情况是什么,那就是一开始匹配成功,如“googlegood”中找“google”,时间复杂度为O(1),未在开始匹配成功的情况的时间复杂度为O(n+m),其中n为主串长度,m为子串长度。根据等概率原则,平均为(n+m)/2次查找,时间复杂度为O(n+m)。

        最坏的情况极其极端,每次匹配不成功的例子都在最后一个字符,比如以下情况:

        T在前40个位置都要判断10次子串是否依次匹配;若到子串T未能匹配,到了第40位,也不会再继续判断下去;因此最坏的时间复杂度为O((n-m+1)*m)

        在实际运用中,计算机中处理的都是二进制的0和1的串,ascll码且汉字也可以看做许许多多个0和1的串组成;所以在计算机运算中,模式匹配操作可以说是随处可见,此算法就显得有些低效了。

KMP模式匹配算法

       

概念引入

        若主串S=“abcdefg”,而子串为D=“abcdex”,当子串中的首元素“a”与主串中的首元素“a”相匹配时,即使子串后续元素未能匹配上,因为判断过子串与主串首元素相等,所以意味着第二位“b”是不需要判断也知道它们是不可能相等的。

        而之后同理,因为子串前5个元素都判断过与主串前5个元素相等,所以到第六位之前皆不需要判断。而在第六位时,也可能出现S[6]=D[1]的情况,所以保留这一步。

       

那么,若子串后续元素中也出现了前继元素相等的情况怎么分析? 

        假设主串S=“abcabcabc”,而子串T=“abcabx”,初次判断时,前5个字符完全相等,而第6个字符不等。

        

        所以判断主串中2,3步可以不再判断。

        而后续元素的判断图解如下 

        我们可以发现,朴素算法中的主串i值回溯是可以不必要的。既然如此,我们就来分析子串中的j值,我们可发现如果主串与子串有相同字符,j值变化就会不同。也就是说,j值变化与主串无关,关键取决于子串结构是否有重复问题。

如图所示 

        

        而影响j值改变的因素和结果,设置一个名为“next数组”的概念来控制。

next数组值推导

        举例,若T=“abcabx”,则表格如下;

  1.         当j=1时,next【1】=0
  2.         当j=2时,j=1~j-1中的字符串为“a”,next【2】=1
  3.         当j=4时,j=1~j=3中的字符串为“abc”,next【4】=1
  4.         当j=5时,j=1~j=4中的字符串为“abca”,此时前缀字符“a”与后缀字符“a”相等,因此推算出k值为2;因此next【5】=2
  5.         当j=6时,j=1~j=5中的字符串为“abcab”,由于前缀字符“ab”与后缀字符“ab”相等,所以k=3,next【6】=3

        我们可以根据以上经验得到,若前后缀一个字符相等,k值是2,n个相等k值就是n+1        

         

        那么,多个重复可能性的字符串,又将怎样判断,如T=“ababaaaba”

  1. 当j=5时,j=1~j=4的字符串为“abab”,前缀“ab”与后缀“ab”相同;k=3
  2. 当j=6时,j=1~j=5的字符串为“ababa”,前缀“aba”与后缀“aba”相同;k=4
  3. 当j=9时,j=1~j=8的字符串为“ababaaab”,前缀“ab”与后缀“ab”相同;k=3

        若T=“aaaaaaab”,又会怎样判断

当j=9时,j=1~j=8的字符串为“aaaaaaaa”,前缀“ax7”与后缀“ax7”相同;k=8

KMP模式算法实现

计算子串next数组的代码

图解该算法 

一、初始与第一次循环

二、第二次循环 

三、第三次循环

      小结:从上方图示我们可以得出,子串推导数组也有两个指针,一个负责推动下标,一个负责回溯。推动下标的i也与next数组息息相关;而回溯的j则是用来进入循环(当前一个字符与后一个字符不等时)与设置next数组的元素值。

        我们还看出了i值的推动(next数组下标的推动及next的赋值)只与进入循环有关。

        另外,next数组推导还有字符串首元素不存放长度的及首字符数组索引为0的kmp算法->计算next数组

正式开始与主串匹配的代码

图解该代码步骤

一、初始状态且第一次循环

二、主串与子串字符不匹配,子串指针回溯

三、回溯后再一次进行匹配,这一次匹配上了,主串指针后移,可主串第二个字符匹配不上子串第二个字符 

四、子串指针回溯到子串首字符后,仍匹不上主串当前的首字符,再一次回溯到0后进入判断条件后主串指针,子串指针皆后移一位。 

kmp模式匹配算法小结 

        由此可见,相对于朴素匹配的算法,kmp匹配算法改动不算大,关键就是去掉了i值回溯的部分。对next数组算法(get-next函数),若T的长度为m,则时间复杂度为O(m),而在kmp匹配算法中(index-KMP),由于i值不回溯,while循环时间复杂度为O(n)。因此,整个算法时间复杂度为O(m+n)。

        这里也需强调,KMP算法仅当模式与主串之间存在许多“部分匹配”的情况下才体现出它的优势。

KMP模式匹配算法改进

         当主串S=“aaaabcde”,子串T=“aaaaax”,其next数组值分别为012345,当kmp匹配算法执行到一定程度会出现以下状况。

        

        有什么办法能简化上方步骤吗?可以尝试用next[1]的值取代与它相等的字符后续next[j]的值。 

        假设取代的数组为nextval,其数组值推导方式如下(nextval数组的推导):

  1.         nextval[1]=0
  2.         本位字符与next数组所对应字符相比较,若相等,则nextval数组为所对应的nextval[next]的值,若不相等,则为next数组所对应的值。

图解如下

一、当i=3时,分析如下

        i=1时,nextval【1】=0;

        i=2时,T【2】=b;T【next【2】】=T【1】=a;所以nextval【2】=next【2】=1;

        i=3时,T【3】=a;T【next【3】】=T【1】=a;所以nextval【3】=nextval【1】=0;

代码如下 

        黑色加粗部分为增加改进的nextval数组代码部分,此时nextval数组已经取代了next数组的功能并增加了匹配失败时回溯方便的功能。

        实际匹配算法,只需将“get-next”改为“get-nextval”即可。关于KMP算法和更详细的说明,可以参考《算法导论》 第二版第32章字符串匹配。

        

  

      

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值