串
定义:是由零个或多个字符组成的有限序列,又名叫字符串。
串的比较
串的比较是通过组成串的字符之间的编码来进行的,而字符的编码指的是字符在对应字符集中的序号。
ASCII编码,由7位二进制数表示一个字符,总共可以表示128个字符
扩展ASCII码,由8位二进制数表示一人字符,总共可以表示256个字符
Unicode编码由16位二进制数表示一个字符,总共就可以表示216 约6.5万多个字符,当然为了兼容ASCII前256个字符与ASCII码相同。
所以我们在C语言中比较两个串是否相等,必须是它们串的长度以及它们各个对应位置的字符都相等时,才算是相等。
串的抽象数据类型
串的逻辑结构和线性表很相似,不同之处在于串针对的是字符集,也就是串中的元素都是字符。
不同之处:对于串的基本操作与线性表是有很大差别的。线性表更关注的是单个元素的操作,比如查找一个元素,插入或删除一个元素,但串中的更多的是查找子串位置、得到指定位置子串、替换子串操作。
ADT 串 (string)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继关系
Operation
StrAssign(T,*chars):生成一个其值等于字符串常量chars的串T
StrCopy(T,S):串S存在,由串S复制得串T
ClearString(S):串S存在,将串清空。
StringEmpty(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)-pos+1,用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)-len+1。从串S中删除第pos个字符起长度为len的子串
endADT
我们来看一个Index的实现算法。
int Inext(String S,String T,int pos)
{
int n,m,i;
String sub;
if(pos>0)
{
n=StringLength(S); //主串长度
m=StringLength(T);//子串长度
i=pos;
while(i<=n-m+1)
{
SubString(sub,S,i,m);
if(StrCompare(sub,T)!=0)
++i;
else
return i;
}
}
}
串的存储结构
串的顺序存储结构
串的顺序存储结构是用一组地址连续的存储单元来存储串中的字符序列的。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般是用定长数组来定义。既然是定长数组,就存在一个预定义的最大串长度,一般可以将实际中长度值保存在数组的0下标位置。但也有些编辑语言不想这么干,觉得存个数字占个空间麻烦。它规定在串值后面加一个不计入串长度的结束标记字符。比如“\0”。这个时候你想知道长度就需要遍历计算一下。其实还是占用一个空间。
其实顺序存储方式其实是有问题的,因为字符串的操作,比如两串的连接Concat、新串的插入StrInsert,以及字符串的替换Replace,都有可能使得串序列的长度超过数组的长度MaxSize。
于是对于串的顺序存储,有一些变化,串值的存储空间可在程序执行过程中动态分配而得。比如在计算机中存在一个自由存储区,叫做“堆”。这个堆可以由C语言的动态分配函数malloc()和free()来管理。
串的链式存储结构
对于串的链式存储结构,与线性表是相似的,但由于串结构的特殊性,结构中的每个元素数据是一个字符,如果也简单的应用链表存储串值,一个结点对应一个字符,就会存在很大的空间浪费。因此一个结点可以存放多个字符,最后一个结点若是未被占满时,可以用#或其他非串值字符补全。当然一个结点存多少个字符才合适变得很重要,这会直接影响着串处理的效率,需要根据实际情况做出选择。但串的链式存储结构除了在连接串与串操作时有一定方便之外,总的来说不如顺序存储灵活。性能也不如顺序存储结构好。
朴素的模式匹配算法
这种子串的定位操作通常称做串的模式匹配。
简单的说,就是对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开做T的长度的循环,直到匹配成功或全部遍历完成为止。
KMP模式匹配算法
克努特---莫里斯---普拉特算法
原理的推导过程还是去看书吧……(大话数据结构那本书的5.7.1那本书里)
我们把T串各个位置的j值的变化定义为一个数组next,那么next的长度就是T串的长度。于是我们可以得到下面的函数定义
next数组值推导
具体如何推导出一个串的next数组呢,我们来看下例
1. T=”abcdex”
j | 123456 |
模式串T | abcdex |
next[j] | 011111 |
1) 当j=1时,next[1]=0;
2) 当j=2时,j由1到j-1就只有一个字符“a”,属于其他情况next[2]=1;
3) 当j=3时,j由1到j-1串是”ab”,显然“a”与“b”不相等,属其他情况,next[3]=1;
4) 以后同理,所以最终此T串的next[j]为011111。
2. T=“abcabx”
j | 123456 |
模式串T | abcabx |
next[j] | 011123 |
1) 当j=1时,next[1]=0;
2) 当j=2时,同上例说明,next[2]=1;
3) 当j=3时,同上,next[3]=1;
4) 当j=4时,同上,next[4] =1
5) 当j=5时,此时j由1到j-1的串是abca,前缀字符”a”与后缀字符”a”相等 根据p1…pk-1=pj-k+1…pj-1得p1=p4
6) 当j=6时,j由1到j-1的串是abcab next[6]=3;
后缀“ab”相等,所以next[6]=3.
我们可以根据经验得到如果前后缀一个字符相等,k值是2,两个字符k值是3,n个相等k值就是n+1;
KMP模式匹配算法实现
//通过计算返回子串T的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]){
++i;
++j;
next[i]=j;
}else{
j=next[j]; //若字符不相同,则j值回溯
}
}
}
//返回子串在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0
int Index_KMP(String S, String T,int pos)
{
int i=pos;
int j=1;
int next[255] ; //定义一next数组
while(i<=S[0]&& j<=T[0])
{
if(j==0|| S[i]==T[j])
{
++i;
++j;
}else{
j=next[j];
}
}
if(j>T[0])
return i=T[0];
else
return 0;
}
KMP算法仅当模式与主串之间存在许多“部分匹配”的情况下才体现出它的优势,否则两者差异不明显。
改进的KMP模式匹配算法
void get_nextval(String T,int *nextval)
{
int i,j;
i=1;
j=0;
nextval[1]=0;
while(i<T[0]) //T[0]表示串T的长度
{
if(j==0||T[i]==T[j]){
++i;
++j;
if(T[i]!=T[j])
nextval[i]=j;
else
nextval[i] = nextval[j];
}else{
j=nextval[j]; //若字符不相同,则j值回溯
}
}
}
更详细的KMP算法数学证明和更详细的说明参阅《算法导论》第2版第32章字符匹配。