1. 串的基本概念
- 串:即字符串(String)是由零个或多个字符组成的有限序列。
- 串的长度:中字符的个数 n,n = 0 n = 0n=0 时的串称为空串。
- 子串:串中任意个连续的字符组成的子序列。
- 主串:包含子串的串。
- 字符在主串中的位置:字符在串中的序号。
- 子串在主串中的位置:子串的第一个字符在主串中的位置 。
2. 串的基本操作
- StrAssign(&T, chars):赋值操作。把串 T 赋值为 chars。
- StrCopy(&T, S):复制操作。由串 S 复制得到串 T。
- StrEmpty(S):判空操作。若 S 为空串,则返回 TRUE,否则返回 FALSE。
- StrLength(S):求串长。返回串 S 中元素的个数。
- ClearString(&S):清空操作。将 S 清为空串。
- DestroyString(&S):销毁串。将串 S 销毁(回收存储空间)。
- Concat(&T, S1, S2):串联接。用 T 返回由 S1 和 S2 联接而成的新串 。
- SubString(&Sub, S, pos, len):求子串。用 Sub 返回串 S 的第 pos 个字符起长度为 len 的子串。
- Index(S, T):定位操作。若主串 S 中存在与串 T 值相同的子串,则返回它在主串 S 中第一次出现的位置;否则函数值为 0。
- StrCompare(S, T):比较操作。若 S>T,则返回值>0;若 S=T,则返回值=0;若 S<T,则返回值<0
3. 串的存储实现
串的顺序存储(静态数组):
// ch[0]废弃不用,声明int型变量length来存放串的长度
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
// 串的初始化
bool InitString(SString &S){
S.length = 0;
return true;
}
// 求串的长度
int StrLength(SString S){
return S.length;
}
// 求主串由位序pos开始len长度的子串存入到串Sub中
bool SubString(SString &Sub, SString S, int pos, int len){
if(pos+len-1 > S.length)
return false;
for(int i=pos; i<pos+len; i++)
Sub.ch[i-pos+1] = S.ch[i];
Sub.length = len;
return true;
}
// 比较S与T的大小。若S>T则返回值大于0;若S=T则返回值等于0;若S<T则返回值小于0
int StrCompare(SString S, SString 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;
}
// 定位串T在串S中的位置,若无法定位则返回0
int Index(SString S, SString T){
int i=1, n=StrLength(S), m=StrLength(T);
SString sub; //用于暂存数据
while(i<=n-m+1){
SubString(sub, S, i, m);
if(StrCompare(sub, T)!=0)
++i;
else
return i;
}
return 0;
}
void test{
SString S;
InitString(S);
...
}
串的顺序存储(动态数组):
#define MAXLEN 255
typedef struct{
char *ch;
int length;
}HString;
bool InitString(HString &S){
S.ch = (char *)malloc(MAXLEN * sizeof(char));
if(S.ch == NULL)
return false;
S.length = 0;
return true;
}
void test{
HString S;
InitString(S);
...
}
串的链式存储:
typedef struct StringNode{
char ch; //每个结点存1个字符
struct StringNode *next;
}StringNode, *String;
上述方式存储密度很低,可以使每个结点存储多个字符。每个结点被称为块,整个链表被称为块链结构。
typedef struct StringNode{
char ch[4]; //每个结点存多个字符
struct StringNode *next;
}StringNode, *String;
4. 串的朴素模式匹配
- 串的模式匹配:在主串中找到与模式串相同的子串,并返回其所在主串中的位置。
- 朴素模式匹配算法思想:将主串中与模式串长度相同的子串搞出来,挨个与模式串对比。当子串与模式串某个对应字符不匹配时,就立即放弃当前子串,转而检索下一个子串。
- 串的朴素模式匹配算法代码实现:
// 在主串S中找到与模式串T相同的子串并返回其位序,否则返回0
int Index(SString S, SString T){
int k=1;
int i=k, j=1;
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)
return k;
else
return 0;
}
时间复杂度:设模式串长度为m,主串长度为n
- 匹配成功的最好时间复杂度:O ( m ) O(m)O(m)
- 匹配失败的最好时间复杂度:O ( n ) O(n)O(n)
- 最坏时间复杂度:O ( n m ) O(nm)O(nm)
5. KPM算法
- 朴素模式匹配算法的缺点:当某些子串与模式串能部分匹配时,主串的扫描指针 i 经常回溯,导致时间开销增加。最坏时间复杂度 O ( n m )
- 串的前缀:包含第一个字符,且不包含最后一个字符的子串。
- 串的后缀:包含最后一个字符,且不包含第一个字符的子串。
- next 数组的计算方法:当第 j 个字符匹配失败时,将前(1~j-1)个字符组成的串记为S,则:next[j]=S的最长相等前后缀长度+1。 特别地,next[1]=0
例:求模式串“abcabd”的 next 数组。
序号j | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
模式串 | a | b | c | d | e | f |
next[j] | 0 | 1 | 1 | 1 | 2 | 3 |
- KPM 算法:当子串和模式串不匹配时,主串指针 i 不回溯,模式串指针 j=next[j]。KMP 算法的平均时间复杂度为:O ( n + m )
- KPM 算法代码实现:
// 获取模式串T的next[]数组
void getNext(SString T, int next[]){
int i=1, j=0;
next[1]=0;
while(i<T.length){
if(j==0 || T.ch[1]==T.ch[j]){
++i; ++j;
next[i]=j;
}else
j=next[j];
}
}
// KPM算法,求主串S中模式串T的位序,没有则返回0
int Index_KPM(SString S, SString T){
int i=1, j=1;
int next[T.length+1];
getNext(T, next);
while(i<=S.length && j<=T.length){
if(j==0 || S.ch[i]==T.ch[j]){
++i; ++j;
}else
j=next[j];
}
if(j>T.length)
return i-T.length;
else
return 0;
}
int main() {
SString S={"ababcabcd", 9};
SString T={"bcd", 3};
printf("%d ", Index_KPM(S, T)); //输出9
}
- KPM 算法的进一步优化:改进 next 数组:
void getNextval(SString T, int nextval[]){
int i=1,j=0;
nextval[1]=0;
while(i<T.length){
if(j==0 || T.ch[i]==T.ch[j]){
++i; ++j;
if(T.ch[i]!=T.ch[j])
nextval[i]=j;
else
nextval[i]=nextval[j];
}else
j=nextval[j];
}
}