五、串
1.串的定义
串(string)是由零个或多个字符组成的有限序列,又名叫字符串。
一般记为 s=“a1a2······an”(n≥0)。
串中的字符数目n称为串的长度。
零个字符的串称为空串(null string)。
空格串:只包含空格的串。
子串与主串:串中任意个数的连续字符组成的子序列称为该串的子串,相应地,包含子串的串称为主串。
2.串的比较
给定两个串:s=“a1a2······an”,t=“a1a2······am”,当满足以下条件之一时,s<t。
-
n<m,且ai=bi(i=1,2,······,n)
例如,当s=“hap”,t=“happy”,就有s<t。因为t比s多两个字母。
-
存在某个k≤min(m, n),使得ai=bi(i=1,2······,k-1),ak<bk。
例如,当s=“happen”,t=“happy”。
3.串的抽象数据类型
ADT 串(string)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继关系
Operation
StrAssign(T, *chars): 生成一个其值等于字符串常量chars的串T
StrCopy(T, S): 串S存在,由串S复制得串T
ClearString(S): 串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): 1≤pos≤StrLength(S),用Sub返回串S的第pos个字符起长度为len的子串
Index(S, T, pos): 1≤pos≤StrLength(S),若主串中存在和串T值相同的子串,则返回他在主串S中第pos个字符之后第一次出现的位置,否则返回0
Replace(S, T, V): 用V替换主串S中出现的所有与T相等的不重叠的子串
StrInsert(S, pos, T): 1≤pos≤StrLength(S),在串S的第pos个字符之前插入串T
StrDelete(S, pos, len): 1≤pos≤StrLength(S),从串S中删除第pos个字符起长度为len的子串
endADT
这里看一下Index的实现:
int Index(String S, String T, int pos) {
int n,m,i;
String sub;
if (pos>0) {
n = StrLength(S);
m = StrLength(T);
i = pos;
while (i <= n-m+1) {
SubString(sub, S, i, m);
if (StrCompare(sub, T) != 0)
i++;
else
return i;
}
}
return 0;
}
4.串的存储结构
4.1 串的顺序存储结构
串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。按照预定义大小,为每个定义的串变量分配一个固定长度的储区。一般是用定长数组来定义。
既然是定长数组,就存在一个预定义的最大串长度,一般可以将实际的串长度值保存在数组的0下标位置,有的书中也会定义存储在数组的最后一个下标位置。但也有些编程语言是规定在串值后面加一个不计入串长度的结束标记字符,比如“\0”来表示串值的终结,这时候如果想要知道此时的串长度,就需要遍历计算一下才知道,但其实这还是需要占用一个空间。
这种串的顺序存储方式还有一个问题,因为字符串的操作,比如两串的连接Concat、新串的插入StrInsert,以及字符串的替换Replace,都有可能使得串序列的长度超过了数组的长度MAXSZIE。
于是对于串的顺序存储,有一些变化,串值的存储空间可在程序执行过程中动态分配而得。比如在计算机中存在一个自由存储区,叫做“堆”。这个堆可由C语言的动态分配函数malloc()和free()来管理。
4.2 串的链式存储结构
对于串的链式存储结构,与线性表是相似的,但由于串结构的特殊性,结构中的每个元素数据是一个字符,如果也简单的应用链表存储串值,一个结点对应一个字符,就会存在很大的空间浪费。因此,一个结点可以存放一个字符,也可以考虑存放多个字符,最后一个结点若是未被占满时,可以用“#”或其他非串值字符补全,如图。
当然,这里一个结点存多少个字符才合适就变得很重要,这会直接影响串处理的效率,需要根据实际情况做出选择。
但串的链式存储结构除了在连接串与串操作时有一定方便之外,总的来说不如顺序存储灵活,性能也不如顺序存储结构好。
5.朴素的模式匹配算法
子串的定位操作通常称作串的模式匹配。
假设我们要从下面的主串S="goodgoogle"中,找到T="google"这个子串的位置。
- S第一位开始,S与T前三个字母都匹配成功,但S第四个字母是d而T的是g。第一位匹配失败。如图所示,其中竖线表示相等,闪电状弯折连线表示不等。
- 主串S第二位开始,主串S首字母o,要匹配的T首字母是g,匹配失败。
- 主串S第三位开始,主串S首字母o,要匹配的T首字母是g,匹配失败。
- 主串第四位开始,主串S首字母d,要匹配的T首字母是g,匹配失败。
- 主串S第五位开始,S与T,6个字母全匹配,匹配成功。
简单的说,就是对主串的每一个字符作为子串的开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或全部遍历完成为止。
前面我们已经用串的其他操作实现了模式匹配的算法Index。现在考虑不用串的其他操作,而是只用基本的数组来实现同样的算法。注意我们假设主串S和要匹配的子串T的长度存在S[0]与T[0]中。代码如下:
/* 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。 */
/* T非空,1≤pos≤StrLength(S)。 */
int Index(String S, String T, int pos) {
int i = pos; // i用于主串S中的当前下标
int j = 1; // j用于子串T中的当前下标
while (i <= S[0] && j <= T[0]) { // 若i小于S长度且j小于T长度时循环
if (S[i] == T[j]) {
i++;
j++;
} else { // 指针后退重新开始匹配
i = i-j+2; // i退回到上次匹配首位的下一位
j = 1; // j退回到子串T的首位
}
}
if (j > T[0])
return i - T[0];
else
return 0;
}
最好情况:
第一次就匹配成功,时间复杂度为O(1)。
平均情况:
时间复杂度为O((n+m)/2)
最坏情况:
时间复杂度为O((n-m+1)*m),例如,主串为S=“00000000000000000001”,子串为T=“00001”,前者有19个"0"和一个"1",后者有4个"0"和1个"1",在匹配时每次都要将T中的字符串循环到最后一位才发现不匹配。
在实际运用中,这种匹配模式太低效了。
6.KMP模式匹配算法
为了解决之前低效的朴素模式匹配算法,三位前辈,D.E.Knuth、J.H.Morris和V.R.Pratt发表一个模式匹配算法,可以大大避免重复遍历的情况,我们称之为克努特-莫里斯-普拉特算法,简称KMP算法。
由于KMP算法比较复杂,这里就不展开讲了,后面单独列出。