1.串的定义
**串,即字符串(String)**是由零个或多个字符组成的有限序列。一般记为S = ‘a1a2…an’(n>=0)
其中S是串名,单引号括起来的字符序列是串的值;ai可以是字母,数字或其他字符;串中字符的个数n称为串的长度。n=0时的串称为空串。
例:S = “HelloWorld!”
T = ‘Hello World!’
- 有的语言用单引号,有的用双引号。
子串:串中任意个连续的字符组成的子序列。(空串也是子串)
例:‘Hello’,'World!'是串T的子串
主串:包含子串的串。
例:T是子串‘Hello’的主串
字符在主串中的位置:字符在串中的序号。
例:’W‘在T中的位置是7(从1开始,空格也算一个字符)
子串在主串中的位置:子串的第一个字符在主串中的位置。(从1开始,空格也算一个字符)
例:'World!'在T中的位置为7
空串:M=‘’
空格串:N=’ ’
2.串的基本操作
串的基本操作,如增删改查等通常以子串为操作对象
- Concat(&T,S1,S2):串联接。将S1和S2连接成新串用T来返回结果。
eg:执行操作Concat(&T,S,W)后:T=“iPhone 11 Pro Max?Pro”
- SubString(&Sub,S,pos,len):求子串。用Sub返回串S的第pos个字符算起的长度为len的子串。
eg:执行操作SubString(&T,S,4,6)后:T=“one 11”
- Index(S,T):定位操作。若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置;如果查找的串T不是S的子串则函数值为0。
eg:执行操作Index(S,W)后,返回值为11
- StrCompare(S,T):比较操作(比较的是该字符在计算机中对应的二进数的大小)。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
在“abandon”和“aboard”的比较中,先比较了前两个值发现都相同,比较第三个值时a<o,所以字符串“aboard”>“abandon”,这里比较的是“a”和"o"在计算机中存储的值,即ASCII码的大小,“a”为97,“o”为111,故a<o。
3.串的存储结构
3.1串的顺序存储
定义静态数组结构:
#define MAXLEN 255 //预定义最大串长为255
typedef struct{
char ch[MAXLEN];//每个分量存储一个字符
int length; //串的实际长度
}SString;
定义动态数组结构存储串:
typedef struct{
char *ch; //按串长分配存储区,指针ch指向串的基地址
int length; //串的长度
}HString;
HString S;
S.ch = (char *) malloc(MAXLEN * sizeof(char));//用malloc函数申请一片连续的存储空间,让基地址指针ch指向这篇空间的起始地址(malloc函数申请的空间需要手动释放)
S.length = 0;
存储方案:
- 用代码实现求子串操作SubString(&Sub,S,pos,len):
//求子串
bool SubString(SString &Sub, SString S, int pos,int len){
//判断操作是否越界
if (pos+len-1 > S.length)
return false;
//把选定范围内的字符赋值到数组ch中
for (int i=pos; i<pos+len; i++)
Sub.ch[i-pos+1] = S.ch[i];
Sub.length = len;//把Sub的length值设为len
return true;
}
- 用代码实现比较操作StrCompare(S,T):
//比较操作。若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0
int StrCompare(SString S, SString T){
//将两个字符串S和T从前往后依次扫描一一对比
for (int i=1; i<=S.length && i<=T.length; i++){
if(S.ch[i]!=T.ch[i])//如果对应位置的值不相等
return S.ch[i]-T.ch[i];//将两个值相减以判断值的大小,并返回结果
}
//扫描过的所有字符都相同,则长度长的串更大
return S.length-T.length;//让数组长度相减并返回结果
}
- 用代码实现定位操作Index(S,T):
int Index(SString S, SString T){
int i=1, n=StrLength(S),m=StrLength(T);//f分别求出数组S和T的长度并存入变量n和m中
SString sub; //用于暂存子串
while(i<=n-m+1){//从头到尾依次取出数组S中长度为m的子串
SubString(sub,S,i,m);//调用取子串操作在数组S中取出长度为m的子串
if(StrCompare(sub, T)!=0)//调用比较操作比较取出的子串与数组T中的字符串是否相同
++i;
else
returni;//返回子串在主串中的位置
}
return 0; //S中不存在与T相等的子串返回0
}
3.2串的链式存储
定义链式存储结构体:
typedef struct StringNode{
char ch[4]; //每个结点存4个字符
struct StringNdoe * next;
}StringNode, * String;
4.串的朴素模式匹配算法
模式串:想尝试在主串中找到的串,未必存在。
串的模式匹配:在主串中找到与模式串相同的子串,并返回其所在位置。
朴素模式匹配算法(简单模式匹配算法)思想:
将主串中与模式串长度相同的子串提出来,挨个与模式串对比,当子串与模式串某个对应字符不匹配时,就立即放弃当前子串,转而检索下一个子串。
代码实现:
int Index(SString S,SString T){
int k=1; //用k来指向当前所对比的子串项的起始位置
int i=k,j=1;//用i和j分别指向两个子串的对应位置
while(i<=S.length && j<=T.length){//判断是否超出字符串范围,没有则继续循环,超出则比较结束
if(S.ch[i]==T.ch[j]){//若比较的值相等
++i;
++j; //继续比较后继字符
}else{ //若不相等则比较下一个位置
k++; //检查下一个子串
i=k;
j=1;
}
}
if(j>T.length) //判断j是否超出边界,超出则代表匹配成功
return K; //返回当前子串的起始位置
else
return 0;
}
- 若模式串长度为m,主串长度为n,则
匹配成功的最好时间复杂度:O(m)
匹配失败的最好时间复杂度:O(n-m+1) = O(n-m)≈O(n)
- 若模式串长度为m,主串长度为n,则知道匹配成功/失败最多需要(n-m+1)*m次比较
最坏时间复杂度:O(nm)
5.KMP算法
5.1主要思想
朴素模式匹配算法的缺点:当某些子串与模式串能部分匹配时,主串的扫描指针i会经常回溯,导致时间开销增加。
KMP算法思路:当子串和模式串不匹配时,主串指针i不回溯,只有模式串指针回溯,模式串指针j=next[j]。
对于模式串“google”进行匹配,用i指向主串中的字符,j指向子串中的字符,将它们进行比对,可以发现:
- 如果j=k时才发现匹配失败,可以说明1~k-1都匹配成功
根据上图可以得到一下结论:
若当前两个字符匹配,则i++,j++;
若j=1时发生不匹配,则应让i++,而j依然是1;
若j=2时发生不匹配,则应让j回到1;
若j=3时发生不匹配,则应让j回到1;
若j=4时发生不匹配,则应让j回到1;
若j=5时发生不匹配,则应让j回到2;
若j=6时发生不匹配,则应让j回到1;
把以上结论定义成一个数组:
5.2KMP算法代码
//在主串S中找到模式串T,传入next数组
int Index_KMP(SString S,SString T,int next[]){
int i=1, j=1;
while(i<=S.length&&j<=T.elngth){
if(j==0||S.ch[i]==T.ch[j]){//比较i和j指向的字母是否相等,当第一个字符就不匹配时把j置0
++i;
++j; //继续比较后继字符
}
else //当i和j指向的字符不相等时执行以下代码
j=next[j]; //改变j指向的位置,模式串右移
}
if(j>T.length)
return i-T.length; //匹配成功
else
return 0;
}
算法平均时间复杂度:O(n+m)
5.3关于模式串的next数组
next数组:当模式串的第j个字符匹配失败时,令模式串跳到next[j]再继续匹配,优先选择上下能够匹配的长度较长的情况进行匹配
串的前缀:包含第一个字符,但不包含最后一个字符的子串
串的后缀:包含最后一个字符,且不包含第一个字符的子串
当第j个字符匹配失败时,由前1~j-1个字符组成的串记为S,则:
next[j] = S的最长相等前后缀长度+1,特别的,next[1] = 0,next[2] = 1
例:对于模式串‘ababaa’:
例:对于模式串‘aaaab’:
5.4KMP算法的优化
-
在用了KMP算法后可以发现,在算法运行了仍然进行了一些不必要的比较步骤,可以对KMP算法进一步优化。
-
可以根据next[j]数组求出一个新的数组nextval[j],该新数组对之前的多余步骤进行了优化,减少了不必要的比较操作。
例:对于模式串‘aaaab’:
- KMP算法优化:当子串和模式串不匹配时j=nextval[j],此时next[j]数组被完全替代;