3 字符串
3.1 C语言字符串
字符串的组成元素是字符,具有如下形式,S = S ,…,S ,其中S 是程序设计语言字母表中的字符。在C语言中将字符串表示成以空字符‘/0’结尾的字符数组(/0不计算在字符串中)。例如有如下字符串及在内存中的表示:
char s[10] = {“dog”};
d | o | g | /0 |
|
|
|
|
|
|
char t[10] = {“house”};
h | o | u | s | e | /0 |
|
|
|
|
或者按如下声明,C编译器就可以分配恰好足够的存储空间保存每个字符(包括空字符/0):
char s[] = {“dog”}; 或char *s = {“dog”};
d | o | g | /0 |
char t[] = {“house”}; 或 char *t = {“house”};
h | o | u | s | e | /0 |
把字符串作为一种抽象数据类型,更加需要关注的是它的操作,比如两个字符串的比较、求字符串长度、字符串连接、求字符串的子串、字符串字串的查找等等。C语言提供了一系列的字符串操作函数,这些函数可以通过声明#include <string.h>来使用。
3.2 普通模式匹配算法及改进
假设有两个字符串string和pat,在string中查找pat,称pat为模式,称这种查找为模式匹配。确定pat是否存在于string中的最简单的方法是使用C语言内置函数strstr,尽管strstr看起来似乎机器适用于进行模式匹配(Pattern Matching),但有两个原因说明为什么还要设计自己的模式匹配函数:
(1) 函数strstr是ANSI C新增加的部分,未必对所有的编译器都可用;
(2) 有多种不同的实现模式匹配函数方法,最简单但效率最低的是顺序检查每个字符直到找到模式或达字符串末端;
对于完全枚举的模式匹配技术,可以进行如下改进:(1)在检查过程中,剩余的字符数小于模式的长度,则停止;(2)检查pat的第一个和最后一个字符与string中对应字符是否匹配。改进后的模式匹配算法在最坏的情况下计算时间仍是 。
3.3 KMP(Knuth, Morris, Pratt)模式匹配算法
一种改进的字符串匹配算法,由D.E.Knuth、V.R.Pratt和J.H.Morris同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。在模式匹配过程中,只有在最坏的情况下,才有必要把模式和字符串中的所有字符都至少检查一次。我们想在字符串中查找模式,无需在字符串中回溯,也就是说当发生失配(比较到字符不等)时,能够根据模式中字符和失配字符在模式中出现的位置,来确定模式中哪个字符与当前字符串中失配字符进行比较。KMP算法就是一种按照上述方式工作的模式匹配算法,该算法具有线性的时间复杂性。
3.3.1 模式匹配算法举例说明
举例说明如下:
设模式pat = ‘a b c a b c a c a b’,
(1) 若字符串string = ‘b ? ? ? … ?’,显然有string[0] != pat[0],可以进行string[1]与pat[0]的比较;
(2) 若字符串string = ‘a c ? ? ? … ?’,显然有string[0]==pat[0],string[1] != pat[1],可以进行string[1]与pat[0]的比较;
(3) 若字符串string = ‘a b d ? ? ? … ?’,显然有string[0]==pat[0],string[1]==pat[1],string[2] != pat[2],若按照完全枚举的模式匹配则此时失配后,下一次是从string[1]和pat[0]开始比较,由于可以从已经匹配的部分得到string[1]==pat[1],且在模式本身可以很容易的得到pat[0] != pat[1],因此对string[1]和pat[0]的比较是多余的,可以直接对string[2]和平pat[0]进行比较,也就实现了字符串中比较的位置不需要回溯;
(4) 若字符串string = ‘ a b c a e ? ? ? … ?’,显然前面4个字符完全匹配,当进行string[4]与pat[4]比较时发生失配,为了使字符串中比较的位置不回溯,下面要计算的是string[4]与pat[4]失配后,string[4]到底要和模式pat中的哪个位置的字符进行比较?显然不可能和pat[4]及其之后的字符进行比较,这时可以把模式向右移动,过程如下:
string: a b c a e ? ? ? … ?’
a b c a b c a c a b <1>初始失配状态string[4]!=pat[4]
string: a b c a e ? ? ? … ?’
a b c a b c a c a b <2>移动1个字符
string: a b c a e ? ? ? … ?’
a b c a b c a c a b <3>移动2个字符
string: a b c a e ? ? ? … ?’
a b c a b c a c a b <4>移动3个字符
可以看到当向右移动3个字符时发生了第一次部分匹配,此时string[2]应该和pat[1]进行比较;那么这个pat中的位置1是怎样计算出来的呢?
有上述例子可以看出当比较到string[4] != pat[4]的时候,我们已经得到了已经匹配的字符串partpat = ’a b c a’,实际上,上述模式向右的移动的问题就可以看做一个partpat相对于另一个partpat的向右移动,直到部分匹配的问题;下面给出几个部分匹配的例子,以便于容易看出规律:
(a) partpat = ‘a b c a’,
partpat = ‘a b c a’,
(b) partpat = ‘a b c a b’,
partpat = ‘a b c a b’,
(c) partpat = ‘a b c a b c’,
partpat = ‘a b c a b c’,
(d) partpat = ‘a b c a b c a’,
partpat = ‘a b c a b c a’,
(e) partpat = ‘a b c a b c a c’,
partpat = ‘a b c a b c a c’,
(f) partpat = ‘a b c a b c a c a’,
partpat = ‘a b c a b c a c a’,
(g) partpat = ‘a b c a b c a c a b’,
partpat = ‘a b c a b c a c a b’,
在上面的7个例子中,假设通过向右移动后的匹配的字符个数为i((a)i=1,(b)i=2,(c)i=3,(d)i=4,(e)i=0,(f)i=1,(g)i=2),可以很明显的看出它们的共同的特点是,从partpat后面向前的i个字符与partpat的前i个字符形成的字符串是相同的;一个字符串由后向前和右前向后的i个字符形成的字符串可能会有很多个,其中i最大也就是形成的字符串最长的一个就是对我们有用的;实际上这个最大的i值就是当发生失配时,字符串失配位置的字符要与之比较的模式中的位置;
3.3.2 失配函数的定义及实现
定义:令 是一个模式,则其失配函数 定义为:
根据失配函数的定义,得到如下模式匹配规则:如果string[i]!=pat[j],则string[i]将和pat[ ]进行比较。
由失配函数的定义,直观的理解为从模式开始处向后和从失配位置failPosition向前寻找长度最大的子串;由于能够形成的最大子串的长度不会超过failPosition-1,因此我们从最长的开始寻找,减少比较的次数,实现函数源码如下:
// 失配函数
// pat 模式字符串
// patLength 字符串长度
// failPosition 失配位置
// 返回-2表示失败
int Mismatch(const char *pat, int patLength, int failPosition)
{
// 返回值
int nFailVal = -2;
// 参数有效性
if (pat==NULL || failPosition<0)
{
return nFailVal;
}
// 字符串长度
int nPatLen = patLength;
// 指定位置错误
if (failPosition >= nPatLen)
{
return nFailVal;
}
// 从前向后计算
nFailVal = -1;
for (int i=failPosition-1; i>=1; i–)
{
if (strncmp(pat, pat+failPosition+1-i, i) == 0)
{
nFailVal = i-1;
break;
}
}
// 返回结果
return nFailVal;
}
上面的函数在实现上很容易理解,可惜不适用,里面出现了多次比较字符串的情况,为此我们可以重新审视3.3.1中的例子:对于(a)而言,是在j=4时发生失配,此时失配函数值为0;对于(b)而言,在j=5时发生失配,求得失配函数返回值为1,通过比较(b)与(a)发现(b)中多出来的最后一个字符恰好等于(a)中失配函数返回位置的字符,即pat[4]==pat[0+1];对于(c)和(b)来说,能够得到同样的结论pat[5]==pat[1+1];依此类推可以根据当前失配位置的字符值与前一个位置的失配函数值处的字符进行比较,如果相等则失配值为前一个位置失配值+1,如果不相等则继续与前面的失配位置的字符比较,直到相等或不存在失配值。为此给出失配函数定义的另外一种表达形式,如下:
根据上述失配函数的表示形式,相应的代码实现如下:
// 失配函数
// pat 模式字符串
// patLength 字符串长度
// failResult 失配位置数组(长度为patLength)
void Mismatch(const char *pat, int patLength, int *failResult)
{
// 第一个位置失配值为-1
failResult[0] = -1;
// 用于记录前一个失配值
int preFailVal = 0;
for (int j=1; j<patLength; j++)
{
// 得到前一个位置失配值
preFailVal = failResult[j-1];
// 若比较字符不等则继续向前比较失配位置的字符
// 直到相等或不存在失配值的位置停止
while (pat[j]!=pat[preFailVal+1] && preFailVal>=0)
{
preFailVal = failResult[preFailVal];
}
// 若相等则当前失配值为前一个失配值+1
if (pat[j] == pat[preFailVal+1])
{
failResult[j] = preFailVal + 1;
}
// 不相等则为-1
else
{
failResult[j] = -1;
}
}
}
3.3.3 模式匹配算法实现
// KMP模式匹配算法
// string 字符串
// pat 待查找模式
// 成功返回模式首字母出现的位置,失败返回-1
int KMPMatch(const char *string, const char *pat)
{
// 参数有效性
if (string==NULL || pat==NULL)
{
return -1;
}
// 计算字符串长度
int lenString = strlen(string);
int lenPat = strlen(pat);
// 比较长度
if (lenPat > lenString)
{
return -1;
}
// 分配失配值数组
int *pFailArray = new int[lenPat];
if (pFailArray == NULL)
{
return -1;
}
// 初始化失配值数组
for (int k=0; k<lenPat; k++)
{
pFailArray[k] = -1;
}
// 计算失配值
Mismatch(pat, lenPat, pFailArray);
// 进行模式匹配
int nFindPos = -1;
int i=0;
int j=0;
while (i<lenString && j<lenPat)
{
// 字符相等
if (string[i] == pat[j])
{
i++;
j++;
}
// 发生失配
else
{
if (j == 0)
{
i++;
}
else
{
j = pFailArray[j-1] + 1;
}
}
nFindPos = (j==lenPat) ? (i–lenPat) : -1;
}
// 释放失配值数组空间
if (pFailArray != NULL)
{
delete[] pFailArray;
pFailArray = NULL;
}
// 返回查找结果
return nFindPos;
}
3.4 小结
本章主要介绍了C语言中字符串的实现及相关字符串的操作,字符串的模式匹配以及KMP模式匹配算法。