串数据类型的定义
串的定义
串是由0个或多个字符组成的有限序列。串中字符的个数称为串的长度,含有0个元素的串叫空串。在C语言中,可以用以下语句定义一个名为str的串。
char str[] = "abcdef";
说明:串通常用一个字符数组来表示。从这个角度来讲,数组str内存储字符为’a’、‘b’、 ‘c’、‘d’、‘e’、‘f’、’\0’,其中’\0’作为编译器识别串结束的标记,而串内字符为’a’、‘b’、 ‘c’、‘d’、‘e’、‘f’,因此数组str的长度为7,而串的长度为6。
串中任意连续的字符组成的子序列称为该串的子串,包含子串的串称为主串,某个字符在串中的序号称为这个字符的位置。通常用子串第一个字符的位置作为子串在主串中的位置。要注意的是,空格也是串字符集合的一个元素,由一个或者多个空格组成的串称为空格串(注意:空格串不是空串)。
串的逻辑结构与线性表类似,串是限定了元素为字符的线性表,从操作集上讲,串与线性表有很大的区别,线性表的操作主要针对表内的某一个元素,而串操作主要针对串内的一个子串。
串的存储结构
- 定长顺序存储表示
一般不采用char str[] = "abcdef";
定义并初始化一个串,原因是仅仅以’\0’作为串结束的标记在求串长时需要扫描整个串,时间复杂度为O(n);不如额外定义一个变量存储串长,这样求串长就变成了时间复杂度为O(1)的操作。
typedef struct string{
char str[maxSize+1]; //str数组的长度为maxSize+1,是因为多出了一个\0
int length;
}Str;
- 变长分配存储表示
变长分配存储表示方法的特点是,在程序执行过程中根据需要动态分配,其结构体的定义
typedef struct str{
char *ch; //指定动态分配存储区首地址的字符指针
int length; //串长度
}Str;
这种存储方式在使用时,需要用函数malloc()
来分配一个长度为length,类型为char的连续存储空间,分配的空间可以用函数free()
释放掉。用函数malloc()
分配存储空间如果成功,则返回一个指向起始地址的指针,作为串的基地址,这个地址由ch指针来指向;如果分配失败,则返回NULL。
串的基本操作
操作定义
//以空串构造字符串
Str* initStr();
//赋值操作,赋值成功返回1。
int strassign(Str* str,char* ch);
//取串长度
int strlength(Str str);
//串比较操作,比较方式:通过ASCII码比较,如果相等,则找下一个。
int strcompare(Str str1,Str str2);
//串连接操作
Str* concat(Str str1,Str str2);
//求子串操作,从pos开始,len长度的子串
Str* substring(Str str,int pos,int len);
//串清空操作
int cleanstring(Str *str);
操作实现
Str* initStr(){
Str* str = (Str*) malloc(sizeof(Str));
str->ch = nullptr;
str->length = 0;
return str;
}
int strassign(Str* str,char* ch){
if(str->ch){ //释放原先串的空间
free(str->ch);
}
int len = 0;
char *c = ch;
while(*c){ //求ch串的长度
++len;
++c;
}
if(len == 0){ //如果ch是一个空串,直接返回空串
str->ch = nullptr;
str->length = 0;
return 1;
}else{
str->ch = (char*) malloc(sizeof(char) * (len + 1));
//取len+1,为了多分配一个空间存放\0
if(str->ch == nullptr){
return 0;
}else{
c = ch;
for (int i = 0; i <= len; ++i,++c) {
//循环的结束符号是<=,len+1,为了将ch的\0复制到新串
str->ch[i] = *c;
str->length = len;
}
return 1;
}
}
return 0;
}
int strlength(Str str){
return str.length;
}
int strcompare(Str str1,Str str2){
for (int i = 0; i < str1.length && i < str2.length; ++i) {
if(str1.ch[i] != str2.ch[i]){
return str1.ch[i] - str2.ch[i];
}
}
return str1.length - str2.length;
}
Str* concat(Str str1,Str str2){
Str *str = (Str*)malloc(sizeof(Str));
str->ch = (char*) malloc(sizeof(char) * (str1.length+str2.length));
int i = 0;
while(i < str1.length){
str->ch[i] = str1.ch[i];
++i;
}
int j = 0;
while(j <= str2.length){
str->ch[i+j] = str2.ch[j];
++j;
}
str->length = str1.length + str2.length;
return str;
}
Str* substring(Str str,int pos,int len){
if(pos<0 || pos>=str.length || len<0 || len>str.length-pos){
return nullptr;
}
Str *substr = (Str*)malloc(sizeof(Str));
if(len == 0){
substr->ch = nullptr;
substr->length = 0;
return substr;
}else{
substr->ch = (char*)malloc(sizeof(char) * (len+1));
int i = pos;
int j =0;
while(i<pos+len){
substr->ch[j] = str.ch[i];
++i;
++j;
}
substr->ch[j] = '\0';
substr->length = len;
return substr;
}
}
int cleanstring(Str *str){
if(str->ch){
free(str->ch);
str->ch = nullptr;
}
str->length = 0;
return 1;
}
字符串模式匹配算法
对于一个串中某子串的定位操作称为串的模式匹配,其中待定位的子串称为模式串。
一般的算法
算法的基本思想:从主串的第一个位置和模式串的第一个字符串开始比较,如果相等,则继续逐一比较后序字符,否则从主串的第二个字符开始,再重新用上一步的方法与模式串中的字符作比较。依次类推,知道比较完模式串中的所有字符,若匹配成功,则返回模式串在主串中的位置;若匹配不成功,则返回一个可区别于主串所有位置的标记,如“0”。
int index(Str str,Str substr){
int i = 0, j = 0,k = i;
while (i < str.length && j < substr.length){
if(str.ch[i] == substr.ch[j]){
++i;
++j;
}else{
j = 1;
i = ++k;
}
}
if(j == substr.length){
return k;
}else{
return -1;
}
}
KMP算法
以ABCABCDHIJK中找出ABCE,串中的位置指针i,j来说明,第一个位置下标以0开始,称为第0位。如果是人为来寻找的话,肯定不会再把i移动回第1位,因为主串匹配失败的位置(i=3)前面除了第一个A之外再也没有A了,为什么能知道主串前面只有一个A?因为已经知道前面三个字符都是匹配的。 移动过去肯定也是不匹配的!有一个想法,i可以不动,只需要移动j即可。
上面的这种情况还是比较理想的情况,我们最多也就多比较了再次。但假如是在串“SSSSSSSSSSSSSA”中查找“SSSSB”,比较到最后一个才知道不匹配,然后i回溯,这个的效率是显然是最低的。
当匹配失败时,j要移动的下一个位置k。存在着这样的性质:最前面的k个字符和j之前的最后k个字符是一样的。 如果用数学公式来表示是这样的P[0 ~ k-1] == P[j-k ~ j-1]。
该规律是KMP算法的关键,KMP算法是利用待匹配的子串自身的这种性质,来提高匹配速度。该性质在许多其他中版本的解释中还可以描述成:若子串的前缀集和后缀集中,重复的最长子串的长度为k,则下次匹配子串的j可以移动到第k位(下标为0为第0位)。将这个解释定义成最大重复子串解释。
这里面的前缀集表示除去最后一个字符后的前面的所有子串集合,同理后缀集指的的是除去第一个字符后的后面的子串组成的集合。举例说明如下:
在“aba”中,前缀集就是除掉最后一个字符’a’后的子串集合{a,ab},同理后缀集为除掉最前一个字符a后的子串集合{a,ba},那么两者最长的重复子串就是a,k=1;
在“ababa”中,前缀集是{a,ab,aba,abab},后缀集是{a,ba,aba,baba},二者最长重复子串是aba,k=3;
在“abcabcdabc”中,前缀集是{a,ab,abc,abca,abcab,abcabc,abcabcd,abcabcda,abcabcdab},后缀集是{c,bc,abc,dabc,cdabc,bcdabc,abcdabc,cabcdabc,bcabcdabc},二者最长重复的子串是“abc”,k=3;
当j为0时,如果这时候不匹配,怎么办? j已经在最左边了,不可能再移动了,这时候要应该是i指针后移。所以在代码中才会有next[0]=-1;这个初始化。
所以,整个KMP的重点就在于当某一个字符与主串不匹配时,应该知道j指针要移动到哪?这里直接讲述计算机如何进行处理。
- 找出前缀pre,设为pre[0~m];
- 找出后缀post,设为post[0~n];
- 从前缀pre里,先以最大长度的s[0~m]为子串,即设k初始值为m,跟post[n-m+1~n]进行比较:
- 如果相同,则pre[0~m]则为最大重复子串,长度为m,则k=m;
- 如果不相同,则k=k-1;缩小前缀的子串一个字符,在跟后缀的子串按照尾巴对齐,进行比较,是否相同。
根据上面的求解过程,我们知道子串的j位前面,有j个字符,前后缀必然少掉首尾一个字符,因此重复子串的最大值为j-1,因此知道下一次的j指针最多移到第j-1位。
求next数组
怎么求这个(这些)k呢?因为在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置j对应的k,所以用一个数组next来保存,next[j] = k,表示当T[i] != P[j]时,j指针的下一个位置。另一个非常有用且恒等的定义,因为下标从0开始的,k值实际是j位前的子串的最大重复子串的长度。请时刻牢记next数组的定义,下面的解释是死死地围绕着这个定义来解释的。
void getNext(Str substr,int next[]){
int j = 0,t = 0;
next[0] = -1;
while (j<substr.length-1){
if(t == -1 ||substr.ch[j] == substr.ch[t]){
next[j+1] = t+1;
++t;
++j;
}else{
t = next[t];
}
}
}
有了next数组下面就是KMP算法
int KMP(Str str,Str substr){
int next[100];
getNext(substr,next);
int i=0,j=0;
while (i < str.length && j < substr.length){
if(j==-1 || str.ch[i] == substr.ch[j]){
++i;
++j;
}else{
j = next[j];
}
}
if(j == substr.length){
return i - substr.length;
}else{
return 0;
}
}