300字范文,内容丰富有趣,生活中的好帮手!
300字范文 > 串--串的定义 顺序 链式存储结构 BF KMP模式匹配算法(C语言描述)

串--串的定义 顺序 链式存储结构 BF KMP模式匹配算法(C语言描述)

时间:2024-06-27 02:57:01

相关推荐

串--串的定义 顺序 链式存储结构 BF KMP模式匹配算法(C语言描述)

此文章仅作为自己学习过程中的记录和总结,同时会有意地去用英文来做笔记,一些术语的英译不太准确,内容如有错漏也请多指教,谢谢!

一、串(String)的定义:

串(String):由零个或多个字符组成的有限序列,又名叫字符串。

一般记作 s=“a1a2…an” (n>=0),其中s是串的名称,用双引号(有些书也用单引号)括起来的字符序列是串的值,注意引号不属于串的内容。ai (1<=i<=n)可以是字母、数字或其它字符。i 就是该字符在串中的位置。串中的字符数目 n 称为串的长度。

这里解释几个概念:

空串(null string):零个字符的串,它的长度为零,可以用两引号""表示,也可以用希腊字母(空集)表示。空格串:指只包含空格的串,是有内容有长度的,而且可以不只有一个空格。子串与主串:串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串。子串在主串中的位置就是子串的第一个字符在主串中的序号。

(eg: “lover”&“over”, “friend”&“end”, “believe”&“lie”)

二、串的比较:

串的比较是通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应字符集中的序号。

因此,要在C语言中比较两个串是否相等,必须是它们串的长度以及它们各个对应位置的字符都相等时,才算是相等。即给定 s1=“a1a2…an”,s2=“b1b2…bn”,当且仅当n=m,且a1=b1, a2=b2, …, an=bn时,才认为s1=s2。

对于两个不相等的串(s1=“a1a2…an”, s2=“b1b2…bm”, n !=m),当满足以下条件之一时,认为s1<s2:

n<m,且ai=bi (i=1,2,…,n)。(eg:“hap”&“happy”)。存在某个k<=min(m,n),使得ai=bi (i=1,2,…,k-1), ak<bk。(eg:“happen”&“happy”)。

(应用:电子词典中查找单词实现的原理,其实就是字符串这种数据结构的典型应用。)

关于上述“对应字符集中的序号”的相关知识:

计算机中的常用字符是使用标准的ASCII编码,由7位二进制数来表示一个字符,总共可以表示128个字符。后来发现一些特殊符号,128个不够用,于是扩展ASCII码由8位二进制数表示一个字符,总共可以表示256个字符。

扩展后的ASCII码已经足够满足以英语为主的语言和特殊符号进行输入、存储、输出等操作的字符需要了。但对于其它语言和文字而言,如中文,256个字符显然是不够的。于是后来就有了Unicode编码,比较常用的是由16位二进制数表示一个字符,这样就总共可以表示216个字符(约为6.5万多个)。

(为了和ASCII码兼容,Unicode编码的前256个字符与ASCII码完全相同。)

ASCII码表:

Unicode码表:

三、串的抽象数据类型:

串的逻辑结构和线性表很相似,不同之处在于串针对的是字符集,也就是串中的元素都是字符。

因此,对于串的基本操作与线性表是有很大差别的。线性表更关注的是单个元素的操作,比如查找、插入或删除一个元素;而串中更多的是查找子串位置、得到指定位置的子串、替换子串等操作。

Data: 串中元素仅由一个字符组成,相邻元素具有前驱和后驱的关系。

[ADT of strings:]Operations:StrAssign(T, *chars): 生成一个其值等于字符串常量chars的串T。StrCopy(T, S): 串S存在,由串S复制得到串T。ClearString(S): 若串存在,将串清空。StringEmpty(S): 若串S为空,返回true,否则返回false。StrLength(S): 返回串S的元素个数,即串的长度。StrCompare(S, T): 若S>T,返回值>0;若S==T,返回值=0;若S<T,返回值<0。Concat(T, S1, S2): 用T返回由S1和S2连接而成的新串。SubString(Sub, S, pos, len): 串S存在,1<=pos<=StrLength(S),且0<=len<=StrLength(S)+1-pos。用Sub返回串S的第pos个字符起长度为len的子串。Index(S, T, pos): 串S和T存在,T是非空串,1<=pos<=StrLength(S)。若主串S中存在和串T值相同的子串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则返回0。Replace(S, T, V): 串S,T和V存在,T是非空串。用V替换主串S中出现的所有与T值相等的不重叠的子串。StrInsert(S, pos, T): 串S和T存在,1<=pos<=StrLength(S)+1。在串S的第pos个字符之前插入串T。StrDelete(S, pos, len): 串S存在,1<=pos<=StrLength(S)+1-len。从串S中删除第pos个字符起长度为len的子串。endADT

此处来看一下Index操作的具体实现:

首先,为了示意清楚以及方便,先声明定义:

#define OK 1 #define ERROR 0#define TRUE 1#define FALSE 0typedef int SElemType; //SElemType的类型根据实际需求而定,此处假设为int。 typedef int Status;/*"Status" is defined as a type of a function, with its returned value representing the result of the function.(Status是函数的类型,它的返回值势函数结果状态代码,0或1.)*/

接下来是Index(S, T, pos)的实现代码:

[Achieve the operation of "Index".]/*T为非空串。若主串S中第pos个字符之后存在与T相等的子串,则返回第一个这样的子串在S中的位置,否则返回0。*/int Index(String S, String T, int pos){int m, n, i;String sub;if (pos > 0){n = StrLength(S);m = StrLength(T);i = pos;while (i <= n+1-m){SubString(sub, S, i, m); //取主串第i个位置,长度与T相等的子串给sub比较。if (StrCompare(sub, T) != 0){i++;}else{return i;}}}return 0;}

四、串的顺序存储结构:

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

既然是定长数组,就存在一个预定义的最大串长度。此处有几种获得串长度值的方法

一般可以将实际的串长度值保存在该定长数组的0下标位置。有的书中也会定义存储在数组的最后一个下标位置。有些编程语言又规定在串值后面加一个不计入串长度的结束标记字符,如’\0’来表示串值的终结。此时若想知道串的长度,则需要遍历计算了。

不足:由于是用定长数组来定义串变量,串的顺序存储方式其实是容易出问题的。由于字符串的操作,比如两串的连接Concat,新串的插入StrInsert,以及字符串的替换Replace,都有可能使得串的长度超过了数组的长度MAXSIZE。

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

五、串的链式存储结构:

对于串的链式存储结构,与线性表是相似的。但是由于串结构的特殊性(结构中的每个元素数据是一个字符),如果也简单的应用链表存储串值,即一个结点对应一个字符,就会造成很大的空间浪费。因此,一个结点可以存放一个字符,也可以考虑存放多个字符。最后一个结点若是未被占满时,可以用"#"或其他非串值字符补全,如图所示。

当然,此处一个结点存放多少个字符才合适就变得更重要了。这会直接影响着串处理的效率,需要根据实际情况做出选择。

但是串的链式存储结构除了在连接串与串的操作上有一定方便,总的来说还是不如顺序存储结构灵活,性能也不如顺序存储结构,因此我们也不过多讨论了。

(注:“六、BF算法(朴素的模式匹配算法)” 及 “七、KMP模式匹配算法” 部分内容引用自用户@Sirim233333的帖子(算法)通俗易懂的字符串匹配KMP算法及求next值算法,只做笔记用,若有不妥立刻删除,感谢!)

六、BF算法(朴素的模式匹配算法):

BF算法:即暴力(Brute Force)算法,是普通的模式匹配算法,是一种蛮力算法。

BF算法的思想:

将主串S的第一个字符与模式串T的第一个字符进行匹配。若相等,则继续比较主串S的第二个字符和模式串T的第二个字符;若不相等,则比较主串S的第二个字符和模式串T的第一个字符,依次比较下去,直到得出最后的匹配结果。

即:首先S[1]和T[1]比较,若相等,则再比较S[2]和T[2],一直到T[M]为止;若S[1]和T[1]不等,则S向右移动一个字符的位置,再依次进行比较。已知n为主串长度,m为模式串长度。如果存在k (1≤k≤n)且S[k+1…k+m]=T[1…m],则匹配成功;否则失败。

若令n为主串长度,m为模式串长度,则该算法最坏情况下要进行 m * (n-m+1) 次比较,时间复杂度为 O(m*n)

在串的操作中,有BF算法的很典型的体现:对子串的定位操作

对子串的定位操作通常称作“串的模式匹配”,是串操作中最重要的操作之一。

思路简单来说就是:对主串的每一个字符作为子串的开头,与模式串进行匹配。对主串做大循环,每个字符开头做模式串T的长度的小循环,直到匹配成功或者全部遍历完成为止。

上面已通过使用串的其他操作来实现模式匹配的算法Index,现在只用基本的数组来实现同样的算法。

假设主串S和模式串T的长度存在S[0]和T[0]中。实现代码如下:

[Achieve the operation of "Index" with BF algorithm.]/*返回模式串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。*//*串T非空,1<=pos<=StrLength(S)。*/int Index(String S, String T, int pos){int i, j;i = pos; //i表示主串S中当前位置下标。若pos不为1,则从pos位置开始匹配。j = 1; //j表示模式串T中当前位置的下标值。初始化为1,即从头开始。while (i <= S[0] && j <= T[0]) //若i小于主串S长度,且j小于串T的长度时循环。{if (S[i] == T[i]){i++;j++;}else //指针后退,重新开始匹配。{i = i - j + 2; //i退回到上次匹配首位的下一位。j = 1; //j退回到串T的首位。}}if (j > T[0]) //j大于串T长度代表匹配成功。{return i - T[0]; //此时i处于串T末位的下一位,因此要减去一个串T长度。}else{return 0;}}T(n)=O(m*n)

总的来说,BF算法,即朴素模式匹配算法,正如其名Brutal Force,是一种很低效的蛮力算法。很多年前,科学家们认为这种有多个0和1重复字符的字符串,模式匹配需要挨个遍历的算法是非常糟糕的。

于是有三位前辈,D.E.Knuth, J.H.Morris, V.R.Pratt发表了一个模式匹配算法,可以大大避免重复遍历的情况,我们称之为Knuth-Morris-Pratt算法,简称KMP算法

七、KMP模式匹配算法:

由前面所说,朴素算法思路简单暴力,但两个串都需要依次重复遍历,算法的时间复杂度为O(n*m),效率很低。科学家们为了提高效率,发表了一种可以避免重复遍历的算法,我们称之为KMP算法。

一般地,在匹配中,我们是不知道主串的内容的,而模式串是我们自己定义的。

朴素算法中,模式串P的第 j 位失配,我们处理的方法是默认的把串P后移一位。

但我们知道,在前一轮的比较中,串P的前 (j-1) 位与主串S中间对应的某 (j-1) 个元素已经匹配成功了。这就意味着,在一轮的匹配中,我们知道了主串S的部分内容。因此,我们可以利用这些内容,让P多移几位,减少遍历的次数,而非无脑暴力地依次重复遍历。

朴素算法与kmp算法的比较:

朴素算法: 每次失配,S串的索引i定位的本次尝试匹配的第一个字符的后一个。P串的索引j定位到1;T(n)=O(n*m)。KMP算法: 每次失配,S串的索引i不动,P串的索引j定位到某个数。T(n)=O(n+m),时间效率明显提高。

注意,此处的“定位到某个数”,就是接下来要引入的next值其数值实际上就是P往后移的位数。

此处举一个例子:

① 例如,Pj处失配(图中绿色的是Pj),则我们可以确定P1…Pj-1是与Si…Si+j-2相匹配的位置,即它们是对应相等的。

② 假设P1…Pj-1中,P1…Pk-1 (1<k<j)与Pj-k+1…Pj-1 是对应相等的。为了下面说的清楚,我们把这种关系叫做“首尾重合”于是我们可以推出,P1…Pk-1 与Si…Si+j-2。

(我们可以发现,后面一段Pj-k+1…Pj-1 的开头位置是Pj-k+1,结束位置是Pj-1,由j-k+1=j-(k-1)可知,此段的长度与前面一段是相同的。这也是为何我们称之为“首尾重合“。)

③ 接下来要做的就是把模式串右移。

④ 为了表示下一轮匹配时 j定位的地方,我们将其定义为数组next[j],而next[j]的数值就是第j个元素前 j-1个元素首尾重合部分个数加1。(我们可以这样理解:j值的多少取决于当前字符之前的串的前后缀的相似度。)当然,为了能遍历完整,首尾重合部分的元素个数应取到最多,即next[j]应取尽量大的值。

"前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。以”ABCDABD”为例,进行介绍:

”A”的前缀和后缀都为空集,共有元素的长度为0;

”AB”的前缀为[A],后缀为[B],共有元素的长度为0;

”ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

”ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

”ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;

”ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为2;

”ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

⑤ 最后,如果我们知道了一个字符串的next值,那么KMP算法也就很好懂了。相比朴素算法,当发生失配时,i不变,j=next[j]就好了。接下来就是怎么确定next值了。

八、KMP算法 – next数组的简单计算:

我们规定任何一个串,next[1]=0。(next[1]在kmp算法中不会用到)

此处举一个简单例子来演示推导next数组的过程:

总结之后,我们可以得到一个十分重要的公式

九、KMP算法 – 求next数组的算法:

void get_next(String T, int* next){int i, j;i = 1;j = 0;next[1] = 0;while (i < T[0]) //T[0]存放着模式串T的长度。{if (j == 0 || T[i] == T[j]) //T[i]表示后缀的单个字符,T[j]表示前缀的单个字符。{i++;j++;if (T[i] != T[j]) //若当前字符与前缀字符不同。{next[i] = j; //则当前的j为next在i位置的值。}else{next[i] = next[j]; //若与前缀字符相同,则将前缀字符的next值赋next在i位置的值。}}else{j = next[j]; //若字符不相同,则j回溯。}}}T(n)=O(n+m)

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。