目录
5.1 串的定义
串(string)是由零个或多个字符组成的有限序列,又名叫字符串。
5.2 串的比较
比如“silly”、“stupid”这样的同样表达“愚蠢的”的单词字符串,它们在计算机中的大小其实取决于它们挨个字母的前后顺序。它们的第一个字母都是“s”,我们认为不存在大小差异,而第二个字母,由于“i”字母比“t”字母要靠前,所以“i”<“t”,于是我们说“silly”<“stupid”。
事实上,串的比较是通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应字符集中的序号。
那么对于两个串不相等时,如何判定它们的大小呢。我们这样定义:
给定两个串:s=“a1a2…an”,t=“b1b2…bm”,当满足以下条件之一时,s<t。
1.n<m,且ai=bi(i=1,2,……,n)。
例如当s=“hap”,t=“happy”,就有s<t。因为t比s多出了两个字母。
2.存在某个k≤min(m,n),使得ai=bi(i=1,2,……,k-1),ak<bk。
例如当s=“happen”,t=“happy”,因为两串的前4个字母均相同,而两串第5个字母(k值),字母e的ASCII码是101,而字母y的ASCII码是121,显然e<y,所以s<t。
5.3 串的抽象数据类型
我们来看一下操作Index的实现算法:
5.4 串的存储结构
串的存储结构与线性表相同,分为两种。
5.41 串的顺序存储结构
串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般是用定长数组来定义。
既然是定长数组,就存在一个预定义的最大串长度,一般可以将实际的串长度值保存在数组的0下标位置,有的书中也会定义存储在数组的最后一个下标位置。但也有些编程语言不想这么干,觉得存个数字占个空间麻烦。它规定在串值后面加一个不计入串长度的结束标记字符,比如“\0”来表示串值的终结,这个时候,你要想知道此时的串长度,就需要遍历计算一下才知道了。
对于串的顺序存储,有一些变化,串值的存储空间可在程序执行过程中动态分配而得。比如在计算机中存在一个自由存储区,叫做“堆”。这个堆可由C语言的动态分配函数malloc()和free()来管理。
5.42 串的链式存储结构
5.5 朴素的模式匹配算法
假设我们要从下面的主串S="goodgoogle"中,找到T="google"这个子串的位置。我们通常需要下面的步骤。
1.主串S第一位开始,S与T前三个字母都匹配成功,但S第四个字母是d而T的是g。第一位匹配失败。如图5-6-1所示,其中竖直连线表示相等,闪电状弯折连线表示不等。
前面我们已经用串的其他操作实现了模式匹配的算法Index。现在考虑不用串的其他操作,而是只用基本的数组来实现同样的算法。注意我们假设主串S和要匹配的子串T的长度存在S[0]与T[0]中。
实现代码如下:
5.6 KMP模式匹配算法
你们可以忍受朴素模式匹配算法的低效吗?也许不可以、也许无所谓。但在很多年前我们的科学家们,觉得像这种有多个0和1重复字符的字符串,模式匹配需要挨个遍历的算法是非常糟糕的。于是有三位前辈,D.E. Knuth、J.H. Morris和V.R. Pratt(其中Knuth和Pratt共同研究,Mor-ris独立研究)发表一个模式匹配算法,可以大大避免重复遍历的情况,我们把它称之为克努特—莫里斯—普拉特算法,简称KMP算法。
5.61 KMP模式匹配算法原理
为了能讲清楚KMP算法,我们不直接讲代码,那样很容易造成理解困难,还是从这个算法的研究角度来理解为什么它比朴素算法要好。
如果主串S=“abcdefgab”,其实还可以更长一些,我们就省略掉只保留前9位,我们要匹配的T=“abcdex”,那么如果用前面的朴素算法的话,前5个字母,两个串完全相等,直到第6个字母,“f”与“x”不等,如图5-7-1的①所示。
接下来,按照朴素模式匹配算法,应该是如图5-7-1的流程②③④⑤⑥。即主串S中当i=2、3、4、5、6时,首字符与子串T的首字符均不等。
似乎这也是理所当然,原来的算法就是这样设计的。可仔细观察发现。对于要匹配的子串T来说,“abcdex”首字母“a”与后面的串“bcdex”中任意一个字符都不相等。也就是说,既然“a”不与自己后面的子串中任何一字符相等,那么对于图5-7-1的①来说,前五位字符分别相等,意味着子串T的首字符“a”不可能与S串的第2位到第5位的字符相等。在图5-7-1中,②③④⑤的判断都是多余。
注意这里是理解KMP算法的关键。如果我们知道T串中首字符“a”与T中后面的字符均不相等(注意这是前提,如何判断后面再讲)。而T串的第二位的“b”与S串中第二位的“b”在图5-7-1的①中已经判断是相等的,那么也就意味着,T串中首字符“a”与S串中的第二位“b”是不需要判断也知道它们是不可能相等了,这样图5-7-1的②这一步判断是可以省略的,如图5-7-2所示。
同样道理,在我们知道T串中首字符“a”与T中后面的字符均不相等的前提下,T串的“a”与S串后面的“c”、“d”、“e”也都可以在①之后就可以确定是不相等的,所以这个算法当中②③④⑤没有必要,只保留①⑥即可,如图5-7-3所示。
之所以保留⑥中的判断是因为在①中T[6]≠S[6],尽管我们已经知道T[1]≠T[6],但也不能断定T[1]一定不等于S[6],因此需要保留⑥这一步。
有人就会问,如果T串后面也含有首字符“a”的字符怎么办呢?
我们来看下面一个例子,假设S=“abcababca”,T=“abcabx”。对于开始的判断,前5个字符完全相等,第6个字符不等,如图5-7-4的①。此时,根据刚才的经验,T的首字符“a”与T的第二位字符“b”、第三位字符“c”均不等,所以不需要做判断,图5-7-4的朴素算法步骤②③都是多余。
因为T的首位“a”与T第四位的“a”相等,第二位的“b”与第五位的“b”相等。而在①时,第四位的“a”与第五位的“b”已经与主串S中的相应位置比较过了,是相等的,因此可以断定,T的首字符“a”、第二位的字符“b”与S的第四位字符和第五位字符也不需要比较了,肯定也是相等的——之前比较过了,还判断什么,所以④⑤这两个比较得出字符相等的步骤也可以省略。
也就是说,对于在子串中有与首字符相等的字符,也是可以省略一部分不必要的判断步骤。如图5-7-5所示,省略掉右图的T串前两位“a”与“b”同S串中的4、5位置字符匹配操作。