一、串
串, 为限制数据类型的线性表,数据对象限定为字符集,如中文字符,英文字符,数字字符,标点字符等,分为
[顺序串],静态开辟或者动态开辟
[链串],为了避免存储密度过低,数据域一般定义数组存储多个元素,数组大小为结点的大小, 其他与链表相同
串:即字符串,为字符组成的有限序列,记为S = ‘a1a2a3a4…an’
子串:串中任意个连续的字符组成的子序列
子串在主串中位置:子串的第一个字符在主串中的位置
类型定义:
//静态开辟顺序串
#define MaxSize 1000
typedef struct
{
char ch[MaxSize];
int length;
}SString;
//动态开辟顺序串
#define MaxSize 255
typedef struct
{
char* ch;
int length;
}HString;
//链串
#define Default 4
typedef struct LNode
{
char ch[Default];
struct LNode* next;
int length;
}LNode,*LinkString;
记录串的长度,也可以不用定义变量length,改用ch[0]来记录长度,这样做的好处是使第一个字符的位序与存储于数组的下标相同,缺点是ch[0]只有一个字节,只能记录0~255的变化范围,串长度超过255不可取
最好的处理办法是闲置ch[0],并额外定义变量length
常用操作函数
Empty(S)
Length(S)
ClearString(&S) 作用等同于InitString
DestoryString(&S)
SubString(&Sub, S, pos, len) 返回串S中pos位置起长度为len的子串给串Sub
StrAssign(&T, chars) 将chars赋值给串T
StrCopy(&T, S) 串S值复制给串T
Concat(&T, S1, S2) 串S1连接串S2返回给串T
StrCompare(S, T) 按序比较字符ASCII码, 若S>T返回>0, 若S<T返回<0, 若字符完全相等且出现S或T无字符进行比较,则返回S.length - T.length
定位操作
int Index(S, T) 若S中存在子串T, 返回第一次出现的位置,否则返回0
二、串的模式匹配
常用操作中的定位操作又称串的模式匹配, 具体算法如下
int Index(SString S, SString T)
{
int n = StrLength(S);
int m = SreLength(T);
SString sub;
for (int i = 1, i < n - m + 1, i++) {
SubString(sub, S, i, m);
if(StrCompare(sub, T) == 0)
return i;
}
return 0;
}
相比之下有更佳算法,如下
朴素模式匹配算法
实质为不调用其他基本操作的最基本定位操作.方法为:
在主串中建立k指针,在k位置开辟出与模式串长度相同的子串,建立子串指针i与模式串指针j并默认指向串头,检索当前字符,若匹配,i,j向右移动继续检索,若不匹配,放弃当前子串,k移动至下一个子串,i,j重置至串头
int Index(SString S, SString T)
{
int k = 1; //k指向主串S当前匹配位置
int i = k; //i指向子串当前对比位置
int j = 1; //j指向模式串T当前对比位置
while (i <= S.length&& j <= T.length) {
if (S.ch[i] == T.ch[j]) { //当前子串位置i和模式串位置j匹配
i++;
j++;
} else { //k游标向右走,i跟随k,j重置至模式串头
k++;
i = k;
j = i;
}
}
if (j > T.length) //j移动到模式串末尾之后代表匹配成功
return k;
else
return 0;
}
KMP算法
对于朴素模式匹配算法,最坏情况是:每次子串指针i和模式串指针j移动到最后一位发现不匹配,故不得不令i回溯到下一个子串的串头,j回溯至模式串的串头,如果坏情况反复出现,即反复出现指针回溯,会造成比较高的时间复杂度
KMP实际是对朴素模式匹配算法的优化,取消子串指针,改为定义在主串按序移动的指针i,模式串指针j与i不匹配时,检索模式串匹配成功部分的前后缀,若有相同前后缀,找出最大的公共前后缀,此后缀位置存储下一次检索过程中匹配成功的部分,向右移动模式串公共前后缀偏移量的长度,使前缀移动到后缀的位置,故j不需要回溯至串头,只有无公共前后缀时,j才回溯至模式串串头.整个过程中i不回溯
第一次发生不匹配时,匹配成功的部分为 ABBAB i指向B,j指向模式串第6位
匹配成功的部分有最大公共前后缀 AB , 偏移量为3
模式串向右移动3个位置,即j指针回溯 3 个位置, 指向模式串第3位,之前的后缀部分成为新匹配成功的部分,此时匹配成功,i,j继续向右移动
此时发生了第二次不匹配,最大公共前后缀偏移量为5
模式串向右移动5个位置,即j指针回溯 5 个位置, 指向模式串第2位,此时虽然i,j匹配,但模式串剩余长度超过主串剩余长度,匹配失败
注:发生不匹配移动模式串后,j指针回溯后的位序为公共前后缀长度 + 1,因此可检索模式串,模拟每个位序发生不匹配时,j应该回溯到的位序.因此可针对模式串建立next数组
p代表模式串的子串元素,存储在此元素下标代表的位序发生不匹配时应回到的位序.
上述过程中求next数组手算比较容易,但代码实现起来很困难,方法思想如下:
- 设立指向最大公共前缀的后一个元素的指针j,与跟随数组位序与模式串对比位置移动的指针i
- 故next[i] = j 则P1P2P3…Pj-1 = Pi-j+1…Pi-1, i指向Pj
- 讨论next[i+1]时Pj = Pi与Pj != Pi的情况
- 若Pj=Pi,则代表P1P2P3…Pj-1 Pj =Pi-j+1…Pi-1Pi,next[i+1] = next[i] + 1
- 若Pj != Pi,视为一个新KMP匹配过程,将P1P2P3…Pj-1 Pj比作模式串与Pi-j+1…Pi-1Pi进行对比,因为Pj != Pi,且P1P2P3…Pj-1与Pi-j+1…Pi-1有相同的最大公共前后缀,故将j回溯,j=next[j],此时j之前的元素与i之前对应数量的字符已经匹配,都为之前两个子串的最大公共前后缀,j指向的字符发生变化,对比新j指针与i指针指向的字符是否匹配,此时又将问题转化为Pj是否等于Pi
代码实现:
int* Build_next(SString T)
{
int* next = (int*)malloc(T.length * sizeof(int));
memset(next, 0, T.length * sizeof(int)); //此时next[1] = 0;
int i = 1, j = 0; //i在模式串上按序移动,j指向模式串i位置前最大公共前缀的后一个元素
while (i < T.length) {
if (j == 0|| T.ch[i] == T.ch[j]) { //j=0代表初始状态或j已经从第1位回溯到0
i++;
j++;
next[i] = j; //j在自增之前是上一个next的元素,
} else {
j = next[j]; //回溯
}
}
return next;
}
KMP算法进行,初始时i,j = 1,
匹配时i,j向右移动继续比对
不匹配时,若j != 1,代表有匹配成功的部分,j回溯至对应数组元素所存储的位序,即j = next[j]并重新进行比对, 若j = 1,代表第一位不匹配,此时next[j] = 0,j回溯到0,i,j向右移动继续比对
如图所示针对模式串建立next数组,假若第9位不匹配,j回溯数组下标9存储的位序,即第3位,若再不匹配,回溯数组下标3存储的位序,即第1位,若再不匹配,i向右移动继续比对
KMP算法代码实现:
int Index_KMP(SString S, SString T)
{
int* next;
next = Build_next(T);
int i = 1; //标记主串
int j = 1; //标记模式串
//S.length - i >= T.length - j 即主串剩余长度不小于模式串剩余长度
while (i <= S.length - T.length + j&& 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;
}
优化的KMP算法
KMP算法相对于朴素模式匹配算法,减少了模式串指针的回溯,增加了效率.但在特殊情况下存在一些问题
如图所示,假如模式串第四位发生了匹配失败的情况,根据其next数组,应令指针j回溯到第一位进行比对,但实际上第四位与第一位的字符完全相同,即j回溯之后的比对必然失败,为了避免这种"无用"的回溯,可以对next数组进行优化
如图所示,当Pj=Pnext[j]时,需要将next[j]递归修正为next[next[j]],修正后进行判定,若Pj=Pnext[next[j]],则需再次递归修正,以此反复直至两者不相等,这样就避免了j指针"无用"的回溯,因此可对Build_next函数做相应修改实现next数组的更新,更新后命名为nextval数组,相应的构建函数名为Build_nextval
代码实现:
int* Build_nextval(SString T) //根据模式串直接建立nextval数组
{
int* nextval = (int*)malloc(T.length * sizeof(int));
memset(nextval, 0, T.length * sizeof(int));
int i = 1, j = 0;
while (i < T.length) {
if (j == 0|| T.ch[i] == T.ch[j]) { //j=0代表初始状态或j已经从第1位回溯到0
i++;
j++;
if (T.ch[i] != T.ch[j]) {
nextval[i] = j; //P~j~!=P~next[j]~ ,nextval值与next数组相同
} else {
nextval[i] = nextval[j]; //否则nextval值进行递归
}
} else {
j = nextval[j]; //回溯
}
}
return nextval;
}
因此,KMP算法的优化,实质是在构建next数组中改为构建nextval数组,在子串与模式串不匹配时,j按照nextval数组的值进行回溯,即j = nextval[j];