代码基本来源:2020王道数据结构考研复习指导 (侵删)
1. 串的定义
即字符串(String)是由零个或多个字符(字母、数字或其他字符)组成的有限序列
-
数据对象:限定为字符集(如中文字符、英文字符、数字字符、标点字符等)
-
基本操作:如增删改查等通常以子串为操作对象
-
重要术语:串长、空串、空格串、子串、主串
-
串长:串中字符的个数n
-
子串:串中任意个连续的字符组成的子序列
-
主串:包含子串的串
-
空串:不包含任何字符的串 M=‘’
-
空格串:M=’ ’ (两个空格)
-
-
易混淆
- 字符在主串中的位置:字符在串中的序号
- 子串在主串中的位置:子串的第一个字符在主串中的位置
2. 串的存储结构
2.1 串的顺序存储
- 物理结构(存储方式):用顺序存储的方式实现的串
- 给各个字符分配连续的存储空间,大小为MaxSize*sizeof(char)
2.1.1 静态数组实现(定长顺序存储)
2.1.1.1 串的定义
#define MaxLen 255//预定义最大串长为255
typedef struct{
char ch[MaxLen];//每个分量存储一个字符
int Length;//串的实际长度
}SString;
2.1.2 动态数组实现(堆分配存储)
2.1.2.1 串的定义及初始化
#define MaxLen 255//预定义最大串长为255
typedef struct{
char *ch;//按串长分配存储区,ch指向串的基地址(串的首字符所在地址)
int Length;//串的实际长度
}HString;
HString S;
S.ch = (char *)malloc(MaxLen * sizeof(char));//按串长动态分配存储区
S.Length = 0;
2.1.3 基本操作的实现
2.1.3.1 求子串 SubString(&Sub,S,pos,len)
- 用Sub返回串S的第pos个字符起长度为len的子串。
- 思路:字符一一赋值到返回串Sub(注意子串范围是否越界)
- 注意
- 子串范围越界的条件 pos+len-1
- 字符一一赋值时,Sub的下标 i-pos+1
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;
}
2.1.3.2 比较操作 StrCompare(S,T) (重要)
- 若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0。
- 返回值为0:S与T长度相等且对应位置的字符相同
- 返回值不为0
- 同为字符但不相等:根据对应ASCII码判断字符大小
- 长串的前缀与短串相同时,长串更大
- 思路:自左向右依次扫描S与T对应位置的字符
- 循环结束条件:任意一个字符串扫描完毕或扫描到两个串中不相同的字符
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;
}
2.1.3.3 定位操作 Index(S,T)
- 若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置;否则函数值为0。
- 思路
- 在主串中从第一个字符开始依次取与所求子串相同长度的子串
- 比较所取子串与所求子串是否相同
- 注意
- 主串中依次取子串时的循环判断条件 i+T.Length-1 < = S.Length(以防所取子串范围越界)
int Index(SString S,SString T){
int i = 1,n = S.Length,m = T.Length;
SString sub;//用于暂存子串
while(i <= n-m+1){
SubString(sub,S,i,m);//从主串取子串
if (StrCompare(sub,T) != 0) ++i;//主串所取子串与所求子串不相同
else return i;//返回子串在主串中的位置
}
}
2.1.4 实现代码的注意
- 串的顺序存储选择以下方案
- ch[0]废弃不用(数组下标<—>位序)
- 设置另外的变量Length记录当前串长(可存储更大的值)
2.2 串的链式存储
2.2.1 串的链式存储的定义
2.2.1.1 每个结点存一个字符(存储密度低)
typedef struct StringNode{
char ch;//每个结点存一个字符
struct StringNode *next;
}StringNode, *String;
2.2.1.2 每个结点存多个字符(存储密度高)
- 若有效字符不能完全填充结点,则结点其余空位用特殊字符填充(‘\0’或’#')
typedef struct StringNode{
char ch[4];//每个结点存多个字符
struct StringNode *next;
}StringNode, *String;
3 串的算法
3.1 朴素模式匹配算法
算法思路:与利用基本操作实现的定位操作Index(S,T)一样
不同点:朴素模式匹配算法不调用其他基本操作
3.1.1 概念
- 子串:一定是主串中存在的
- 模式串:尝试在主串中找到的串,未必存在
- 串的模式匹配:在主串中找到与模式串相同的子串,并返回其所在位置。
3.1.2 思想
- 将主串中与模式串长度相同的子串找出来,逐个字符与模式串对比(SubString(sub,S,i,m))
- 当子串与模式串中某个对应字符不相同时(StrCompare(sub,T) != 0),就立即放弃当前子串,转而检索下一个子串
3.1.3 时间复杂度
模式串长度为m,主串长度为n;一般情况下 n >> m
- 最好时间复杂度(对于每个子串,模式串第一个字符就与子串对应字符不相同)
- 成功:主串第一个子串即与模式串配对上(O(m))
- 失败:主串最后一个子串的第一个字符就不能与模式串配对上(O(n-m+1)) = O(n)
- 最坏时间复杂度(对于每个子串,模式串最后一个字符才与子串对应字符不相同)
- 成功/失败:O(n-m+1) (扫描到主串的最后一个子串) * O(m) (扫描模式串) = O(nm-m^2 +m) = O(nm)
3.1.4 实现代码
- 循环结束条件:i <= S.Length && j <= T.Length
- 子串范围越界,超过主串范围(i > S.Length-T.Length+1 )—>模式串匹配失败
- 指向模式串的指针越界(j > T.Length)—> 模式串匹配成功
int Index(SString S,SString T){
int k = 1;//记录当前检查的子串起始位置
int i = k;
int j =1;//记录指向当前模式串中字符的位置
while(i <= S.Length-T.Length+1 && j <= T.Length){
if (S[i] == T[j]){//模式串与当前检查子串对应字符相等
++j;++i;//继续比较后继字符
}else{//当前检查子串与模式串对应字符不匹配
//检查下一个子串
k++;i = k;//主串中指针回溯,指向当前检查子串的下一个子串
j = 1;//模式串指针回溯到第一个字符
}
}
if (j > T.Length)
return k;//返回当前检查子串的位置
else
return 0;//模式串匹配失败
}
3.2 KMP算法
- 朴素模式匹配算法缺点:当某些子串与模式串能部分匹配时,主串的扫描指针i经常回溯,导致时间开销增加
3.2.1 算法思路
为优化朴素模式匹配算法,引入KMP算法
- 当子串和模式串不匹配时
- 主串的扫描指针i不回溯
- 模式串指针j回溯:模式串指针j=next[j]
- 重新进入循环:比对当前主串的扫描指针i所指字符与模式串指针j=next[j]所指字符
- 代码易错点(重要)
- 模式串匹配成功,返回子串的位置:i - T.Length(子串的位置:子串的首字符在主串中的位置)
int Index_KMP(SString S,SString T,int next[]){
int i = 1,j = 1;
int next[T.Length + 1];//next[0]废弃不用
get_next(T,next);//求模式串的next数组
while (i <= S.Length && j <= L.Length){
if (j == 0 || S.ch[i] == T.ch[j]){
++j;++i;//继续比较后继字符
}else
j = next[j];
}
if (j > T.Length)
return i - T.Length;//返回的与模式串匹配的子串的位置
}
3.2.2 算法关键点(求next[j](由模式串确定))
- 主串的扫描指针i:主串当前检查字符的位置
- 模式串指针j:初始化时的模式串待检查字符的位置
- 模式串扫描指针k:模式串当前检查字符的位置(代码中不需要出现,只是为了方便理解而引入的其他变量)
-
已知条件
- 当 j = k时才发现匹配失败,说明1~ k-1都匹配成功
- 当模式串的第j 个字符匹配失败时,令模式串跳到next[j] 再继续匹配
-
概念
- 串的前缀:包含第一个字符,且不包含最后一个字符的子串
- 串的后缀:包含最后一个字符,且不包含第一个字符的子串
- 部分匹配值:字符串的前缀和后缀的最长相等长度
3.2.2.1 求模式串的next数组(手算)
- 在模式串已匹配的子串S中,求其最长相等前后缀的长度
- j >= 3:当第j个字符匹配失败,由前1~j-1 个字符(已匹配成功的字符)组成的串记为S,则next[j]= S的最长相等前后缀长度+1
- 模式串中已成功匹配的部分S = ‘ababab’
- 其相等前后缀有‘ab’、‘abab’(最长相等前后缀)
- 则next[7 ] = 4+1 = 5
-
j = 1(模式串第一个字符即匹配不上)next[j] =0
- 进入循环后,第一次进入循环:主串扫描指针后移一位,模式串扫描指针指向第一个字符
- 第二次循环及以后:对比对应字符
-
j = 2(模式串第二个字符即匹配不上)next[j] =1
- 模式串中成功匹配的子串长度为1(串的前缀与串的后缀为∅(不存在)—> 最长相等前后缀长度为0)
3.2.2.2 求模式串的next数组(机算)
-
假设 next[j] = k-1+1= k(即’p1…pk-1’ = ‘p j-k+1…p j-1’——已成功匹配的第1-j-1个字符的部分匹配值为k-1)
-
求next[j+1]
- 最好的情况:pj = pk —>next[j+1] = next[j] +1 = k+1
- 假如 pj ≠ pk,则此时求next[j+1]可视为一个模式匹配问题
- 将’p1…pk’向右滑动至第next[k](模式匹配问题)个字符与pj比较(next[k] =在已匹配的’p1…pk-1’中找到部分匹配值+1)
- p[next[k]] = pj---->next[j+1] = next[k]+1(‘p1…pj’ 最长相等前后缀为k)
- p[next[k]] ≠ pj----->继续比较p[next[next[k] ] == pj (终止条件为k==0,此时next[j+1] = 0+1 = 1)
- 将’p1…pk’向右滑动至第next[k](模式匹配问题)个字符与pj比较(next[k] =在已匹配的’p1…pk-1’中找到部分匹配值+1)
]
void get_next(SString T,int next[]){
int j = 1; k = 0;
next[1] = 0;
while(j < T.Length){//串的前(后)缀不包含第一个(最后一个)字符
if ( k == 0 || T.ch[j] == T.ch[k]){
//k == 0 求解next[2] = 1
//T.ch[i] == T.ch[k] 还可用于判断p[next[k]] == p[j]
++j;++k;
//若p[j] = p[k],则next[j+1] = next[j] + 1;
next[j] = k;
}else{
//否则令k = next[k],循环继续
k = next[k];//'p1...pk-1pk模式匹配问题',pk ≠ pj,将'p1...pk'向右滑动至第next[k]个字符
}
}
}
3.2.3 时间复杂度O(m+n)
模式串长度为m,主串长度为n;一般情况下 n >> m
-
模式串匹配:主串指针不回溯O(n)
-
求next[j]数组:O(n)
3.2.4 KMP算法优化 (nextval 数组)
- 避免无效比对:若模式串指针所指当前字符与其回溯后j = next[j]所指字符相同,则多进行了一次无意义的对比
- 优化思路:比对模式串当前所指字符与模式串在当前位置回溯后所指字符是否相等
-
实现思路:next[j]–修正并得到–>nextval[j]
- nextval[1] = 0
- 从j=2开始,对next[j]自左向右扫描(保证next数组从下标小到大的数值依次被修改好,就不用一步一步往前回溯对比,只回溯到前一个对比即可)
- 判断 模式串当前字符 T.ch[j] == 其回溯后所指字符 T.ch[next[j]]
- 相等: next[j] = next[next[j]](修正next[j])
- 不相等:继续判断next数组下一个元素(++j)
- 判断 模式串当前字符 T.ch[j] == 其回溯后所指字符 T.ch[next[j]]
-
实现代码
for (int j = 2;j < T.Length; ++j){
if(T.ch[j] == T.ch[next[j]])
nextval[j] = next[next[j]];//需要改变原next[j]的值
else
nextval[j] = next[j];//不需要改变原next[j]的值
}