文章目录
一、串
1.串的定义
串(String)是由零个或多个字符组成的有限序列。一般记为:
S
=
"
a
1
a
2
.
.
.
a
n
"
(
n
>
=
0
)
S = "a~1~a~2~...a~n~"(n>=0)
S="a 1 a 2 ...a n "(n>=0)
当n=0时的串称为空串。
串中任意多个连续的字符组成的子序列称为该串的子串,包含子串的串称为主串。
某个字符在串中的序号称为该字符在串中的位置。子串在主串中的位置以子串的第1个字符在主串中的位置来表示。当两个串的长度相等且每个对应位置的字符都相等时,称这两个串是相等的。例如:
A=‘ChinaBeijing’,B=‘Beijing’,C=‘China’
它们的长度分别为13、7和5。B和C是A的子串,B在A中的位置是7,C在A中的位置是1。
需要注意的是,由一个或多个空格(空格是特殊字符)组成的串称为空格串(注意,空格串不是空串),其长度为串中空格字符的个数。
串的逻辑结构和线性表极为相似,区别仅在于串的数据对象限定为字符集。在基本操作上,串和线性表有很大差别。线性表的基本操作主要以单个元素作为操作对象,如查找、插入或删除某个元素等;而串的基本操作通常以子串作为操作对象,如查找、插入或删除一个子串等。
2.串的存储结构
1)定长顺序存储表示
typedef struct {
char ch[Maxsize];
int length;
}SString;
串的实际长度只能小于或等于Maxsize,超过预定义长度的串值会被舍去,称为截断。
串长有两种表示方法:一是如上述定义描述的那样,用一个额外的变量length来存放串的长度;二是在串值后面加一个不计入串长的结束标记字符“\0”,此时的串长为隐含值。
2)堆分配存储表示
typedef struct {
char *ch;
int length;
}HString;
在C语言中,存在一个称之为“堆”的自由存储区,并用malloc()和free()函数来完成动态存储管理。
利用malloc()为每个新产生的串分配一块实际串长所需的存储空间,若分配成功,则返回一个指向起始地址的指针,作为串的基地址,这个串由ch指针来指示;若分配失败,则返回NULL。已分配的空间可用free()释放掉。
3)块链存储表示
类似于线性表的链式存储结构,也可采用链表方式存储串值。由于串的特殊性(每个元素只有一个字符),在具体实现时,每个结点既可以存放一个字符,也可以存放多个字符。每个结点称为块,整个链表称为块链结构。
typedef struct LString{
char ch[Size];
struct LString *next;
}LString,*LSlist;
二、串的模式匹配
子串的定位操作通常被称为串的模式匹配,它求的是子串(常称模式串,后续都称模式串)在主串中的位置。例如:
已知主串S=‘abababcababab’,模式串T=‘ababc’
匹配T是否存在于S,若存在则给出T在S的位置。
1.暴力匹配算法
int Index(SString S, SString T) //暴力匹配算法
{
int i = 1, j = 1; //定义“指针”,i指向主串,j指向模式串
while (i <= S.length && j <= T.length) //满足指针i,j都未超过字符串长度
{
if (S.ch[i] == T.ch[j]) //若字符相等
{
++i;
++j; //两指针同时后移一位
}
else //若字符不相等
{
i = i - j + 2; //指针i回退至“本次主串中参与匹配的子串的第二个字符”
j = 1; //指针j回退至模式串的第一个字符
}
}
if (j > T.length) //匹配通过
return i - T.length; //返回对应子串的位置
else //匹配不通过
return 0; //返回0
}
示意图:

匹配通过,指针后移。

… …

匹配失败,指针i回退至i-j+2的位置,即本次主串中参与匹配的子串(‘ababa’)的第二个字符(‘b’),指针j回退至1。

再次匹配失败,指针i回退至i-j+2的位置,即本次主串中参与匹配的子串(‘babab’)的第二个字符(‘a’),指针j回退至1。

… …

匹配通过,指针后移。

j > T.length,匹配成功,返回i-T.length,即3。
由上可以看出,若主串长m,模式串长n,对于暴力匹配算法,其时间复杂度为O(m*n)。
2.KMP算法
暴力匹配算法中,我们会发现这样一个问题,倘若主串为‘0000000000000000000000000000000000000000000001’,而子串为‘0000001’时,直到匹配结束,指针i需要回溯39次,每次回溯都要比较7次,总比较次数达到280次。显然,指针i频繁且不必要的回溯,严重拖慢了算法的执行速度,那么,是否可以使指针i无需回溯,仅移动指针j,而能正常进行匹配的方法呢?
答案自然是肯定的。
1)模式串对齐

首先,我们知道,当字符匹配通过时,指针i和指针j都会后移,并继续匹配下一个字符,那么,根据上图,我们就可以说,当j >= 2时,主串当前参与匹配的子串(‘ababa’)中已匹配通过的子串(‘abab’)与模式串中指针j(不含当前指针j指向的字符)之前的子串(‘abab’)一定相等。
既然相等,那么指针j该如何移动呢?
由上图我们可以发现,子串中‘abab’含有重复结构,移动指针j进行模式串的对齐时仅需移动一个‘ab’即可:

由此我们可以看出,指针j的移动(模式串的对齐)与模式串的结构有关,与主串无关。
那么,模式串的对齐与其结构有何关系?,在这里我们先弄清几个概念。
2)字符串的前缀、后缀和部分匹配值
前缀指除最后一个字符以外,字符串的所有头部子串。
例如‘ababa’,其前缀为:
a,ab,aba,abab
后缀指除第一个字符以外,字符串的所有尾部子串。
例如‘ababa’,其后缀为:
a,ba,aba,baba
部分匹配值(PM) 指字符串的前缀和后缀的最长相等前后缀的长度
例如‘ababa’,其前缀{a,ab,aba,abab}∩后缀{a,ba,aba,baba}={a,aba},最长相等前后缀是aba,长度为3,其部分匹配值为3。
那么它们有什么用呢?
我们先针对字符串‘ababc’求出其部分匹配值表,即分别求出‘a’,‘ab’,‘aba’,‘abab’,‘ababc’的部分匹配值(分别为0,0,1,2,0),得出下表:

然后我们用该PM表来进行字符串的匹配:

同样使用上面的例子,如上图,当匹配至模式串的最后一个字符时与主串的字符不相等,此时已匹配了4个字符,成功匹配的字符串为‘abab’,对照PM表,我们发现‘abab’的PM值为2,那么我们将指针j向左移已匹配的字符数(4)- 已匹配的字符串对应PM值(2) = 2个字符,即 j = 5 - 2 = 3:

如此,我们便明白,指针j的移动位数 = 已匹配的字符数 - 已匹配的字符串对应PM值
PM值的本质就是对模式串结构的分析(换句话说就是根据模式串中的重复片段,算出第一个片段尾在字符串中的位置,然后将指针j指向该片段尾的下一个字符)
3)KMP算法的分析
<1>next数组的产生
已知移动位数 = 已匹配的字符数 - 已匹配的字符串对应PM值与模式串中指针j的值,
可以写成Move = (j - 1) - PM[j-1]
对该式可做进一步改进:
首先,观察‘ababc’的PM表,我们发现:

1)匹配失败时,需要看前一项的PM值
2)当匹配至模式串中的最后一个字符时,使用的是‘abab’的PM值,而‘ababc’对应的PM值,我们是用不到的,可以舍去;
3)同样,当匹配至模式串中的第一个字符时,并无可参照的PM值。
由此,我们可以将PM项后移,为了区分第一个字符,其PM值用-1表示。

这样,原式就变成了Move = (j - 1) - next[ j ]
移动后指针j的值= j - Move = j - ((j - 1) - next[ j ]) = next[ j ] + 1
为了计算方便,我们可以令next数组中的值+1:

此时移动后指针j的值 = next[ j ],即j = next[ j ]
<2>next数组的求取
既然知道了模式串指针j的变化公式,那么我们该如何求取next数组呢?
首先,倘若模式串中仅有一个字符‘a’时,其next值:

注:上面我们在移动PM项前,PM值为 以所对应的字符为末尾,第一个字符为首组成的字符串 的最长相等前后缀的长度;而移动PM项后,我们所要找的 字符串的末尾字符 应为 对应PM值的上一个字符。
那么,如果该模式串存在第二项,由于单独一个字符‘a’没有最长相等前后缀,因此其PM值为0,next值在改进后+1,因此,第二项的next值为1,注:由于上面进行了改进,求出来的PM值要+1。
如图:

倘若存在第三项,且第二项字符为‘b’,则可算出第三项的next为1:

类推直至下图:

如图,由next数组及模式串结构我们发现,模式串中存在重复子串‘ab’
如果存在第七项,且第六项字符为‘a’时:

我们发现,第六项字符为‘a’,恰与第三项字符相等,使得在原先重复子串‘ab’的基础上构成新的重复子串‘aba’,其next值也在其基础上+1。由上面我们得出的j=next[j](指针j的回退本质上是寻找上一个重复结构的下一个字符),可以将其表示为:
if(s[next[6]] == s[6]) //假设模式串名为s
next[6 + 1] = next[6] + 1
注:为方便表示,数据从下标1开始存储,因此第六项的next值表示为next[6],字符表示为s[6]
如此类比推理,倘若存在第八项字符,且第七项字符为‘a’,因为‘a’(即s[7])与s[next[7]] (对应第四个字符‘a’)相等,则next[8] = 4(next[7]) + 1 =5。
那么,如果s[7]与s[next[7]]不相等,那么next该如何计算?
求next函数值的问题也可以视为一个模式匹配的问题,若s[7]与s[next[7]]不匹配,,则寻找下一个s[next[next[7]]]与s[7]匹配,例如,假设第七项字符为‘b’,‘b’(s[7])与‘a’(s[next[7]])不匹配,继续与‘b’(s[next[next[7]]])匹配,成功,则next[8] = 2(next[next[7]]) + 1 =3;假设第七项字符为‘c’,‘c’(s[7])与‘a’(s[next[7]])不匹配,继续与‘b’(s[next[next[7]]])匹配,失败,则继续与‘a’(s[next[next[next[7]]]])匹配。失败,则next[8] = 1。(无最长前后缀)
由此可以得出next公式:

<3>代码实现
void Next(SString T, int next[]) //next数组求取
{
int i = 1, j = 0; //i:模式串指针,j:next值
next[1] = 0; //首字符next值设为0
while (i < T.length) //遍历模式串
{
if (j == 0 || T.ch[i] == T.ch[j]) //是否满足next值为0或对应字符匹配通过
{
++i; //若满足,指针i+1
++j;
next[i] = j; //next值+1,即next[j+1] = next[j] + 1
}
else
j = next[j]; //若不满足,跳至next[j]位置重新匹配
}
}
int Index_KMP(SString S,SString T, int next[]) //KMP算法
{
int i = 1,j = 1; //定义“指针”,i指向主串,j指向模式串
while (i < S.length && j <= T.length) //满足指针i,j都未超过字符串长度
{
if (j == 0 || S.ch[i] == T.ch[j]) ////是否满足next值为0或对应字符匹配通过
{
++i; //若满足,指针后移,继续匹配后续字符
++j;
}
else
j = next[j]; //若不满足,模式串指针回退至目标位置
}
if (j > T.length) //匹配成功
return i - T.length; //返回主串中的对应位置
return 0; //匹配失败
}
<4>KMP匹配过程中比较次数的分析
尽管普通模式匹配的时间复杂度是O(mm),KMP算法的时间复杂度是O(m+n),但在一般情况下,普通模式匹配的实际执行时间近似为O(m+n),因此至今仍被采用。KMP算法仅在主串与子串有很多“部分匹配”时才显得比普通算法快得多,其主要优点是主串不回溯。
4)KMP算法的再优化
<1>nextval数组
前面所定义的next数组在某些情况下仍存在缺陷,例如模式串s =‘aaaab’与主串p=‘aaabaaaab’进行匹配:
| 主串p | a | a | a | b | a | a | a | a | b |
|---|---|---|---|---|---|---|---|---|---|
| 模式串s | a | a | a | a | b | ||||
| j | 1 | 2 | 3 | 4 | 5 | ||||
| next[j] | 0 | 1 | 2 | 3 | 4 | ||||
| nextval[j] | 0 | 0 | 0 | 0 | 4 |
如上图,当j = 4,i = 4时,主串与模式串匹配不通过,若用next数组,主串中p[4]仍需与s[3]、s[2]、s[1]进行比较,但s[4]=s[3]=s[2]=s[1]=‘a’,因此这些比较是无意义的,为避免这种情况,可以再引入一个nextval数组来记录模式串中连续相等的字符数。
<2>nextval数组的实现
void Nextval(SString T, int nextval[]) //nextval数组求取
{
int i = 1, j = 0; //i:模式串指针,j:nextval值
nextval[1] = 0; //nextval第一个值设为0
while (i < T.length) //遍历模式串
{
if (j == 0 || T.ch[i] == T.ch[j]) //是否满足nextval值为0或对应字符匹配通过
{
++i; //若满足,指针后移,继续比较下一个字符
++j; //同时nextval值+1
if (T.ch[i] != T.ch[j]) //若下一个字符匹配未通过
nextval[i] = j; //将nextval值存储至对应位置
else
nextval[i] = nextval[j]; //否则将nextval指向的位置的nextval存储至对应位置
}
else
j = nextval[j]; //回退nextval
}
}

3147

被折叠的 条评论
为什么被折叠?



