串的定义和基本操作
串的定义
串 v.s. 线性表
串的基本操作
//串的基本操作
//赋值操作
StrAssign(&T,chars)
//复制操作
StrCopy(&T,S)
//判空操作
StrEmpty(S)
//求串长
StrLength(S)
//清空操作
ClearString(&S)
//销毁串
DestroyString(&S)
//串联结
Concat(&T,S1,S2)
//求子串
SubString(&Sub,S,pos,len)
//定位操作
Index(S,T)
//比较操作
StrCompare(S,T)
串的比较操作
字符集编码
拓展:乱码问题
串的存储结构
串的顺序存储
串的链式存储
基本操作的实现
#include<stdio.h>
#definde 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)); //用完需要手动free
S.length = 0;
//链式存储
typedef struct StringNode{
char ch;
struct StringNode *next;
//存储密度低:每个字符1B,每个指针4B
}StringNode,*String;
typedef struct StringNode{
char ch[8]; //每个结点存多几个字符,提高存储密度
struct StringNode *next;
}StringNode,*String;
//求子串
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则返回值>0,若S=T则返回值=0,若S<T则返回值<0
int StrCompare(SString S,SString T){
for(int i = 1;i <= S.length; i++){
if(S.ch[i] != T.ch[i]){
return S.ch[i] - T.ch[i];
}
}
//扫描过的所有字符都相同,则长度长的串更大
return S.length - T.length;
}
int Index(SString S,SString T){
int i = 1;
int n = StrLength(S);
int 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; //S中不存在与T相等的子串
}
两种模式匹配算法
朴素模式匹配算法
什么是字符串的模式匹配
- 就是要在字符串里面搜索某一段内容
算法具体实现
int Index(SString S,SString T){
int i = 1;
int n = StrLength(S);
int 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; //S中不存在与T相等的子串
}
//朴素模式匹配算法
int Index(SString S,SString T){
int i = 1;
int j = 1;
while(i <- S.length && j <= T.length){
if(S.ch[i] == T.ch[j]){
i++;
j++
}else{
i = i - j + 2;
j = 1;
}
if(j > T.length){
return i - T.length;
}else{
return 0;
}
}
}
主串S
和模式串T
,同时设置两个扫描指针i
和j
,指针指到哪我们就要把字符对比到哪- 如果字符相等我们就会让
i
和j
分别后移
- 到了第六个字符的时候
i
和j
指向的字符不相等,第一个子串匹配失败,开始匹配第二个子串
i
和j
指向的字符不相等,第二个子串匹配失败,开始匹配第三个子串
- 到了第二个字符的时候
i
和j
指向的字符不相等,第三个子串匹配失败,开始匹配第四个子串
- 一路匹配下来,直到
j
的位置已经超出了模式串的长度,说明当前的子串是匹配成功的,此时应该返回当前子串第一个字符的位置—i-T.length
KMP算法
朴素模式匹配算法优化思路
-
基于朴素模式匹配算法进行优化而得来的
-
当模式串的某一个字符不匹配的时候,这个主串的前边的字符一定和模式串是保持一致的,所以我们要利用好模式串前面几个字符的信息
-
通过模式串的部分匹配,我们可以知道主串里边前边的几个字符到底是什么
-
如果当前的子串匹配失败我们会匹配下一个字串,但是现在我们已经知道了这个子串前边几个字符的信息,发现第一个字符已经和模式串不匹配了,所以当前这个子串其实根本没有必要再进行一个检查和匹配,我们可以直接跳过
-
再来看下一个子串,现在的子串我们也已经知道了它前面几个字符的信息,第一个字符可以匹配,第二个字符不匹配,所以这个子串我们也没有必要检查
-
再来看下一个子串,现在的这个子串,我们由第一次匹配失败也知道它头部的部分字符的信息,这个子串的第一个字符和模式串能够匹配,第二个字符和模式串也能够匹配,但是第三个字符到底是什么我们还不得而知
-
所以对于现在的这个子串来说,前边两个字符是可以匹配的,第三个字符是否能够匹配还暂时不知道,因此对于现在这个字串来说我们从第三个位置开始检查,往后匹配就可以了
-
对于一个模式串T来说,当我们匹配到某一个字符,这个字符发生了失配的时候,那么这个字符之前的这些主串的信息我们是可以确定的,它和模式串一定是保持一致的
-
所以在这个时候我们就没有必要检查以2开头的这个元素,也没有必要检查已3开头的这个元素,因为根据我们已知的信息就可以判定它一定是匹配失败的,所以我们直接对比以4开头的那个子串,我们没有必要对比前两个元素,因为我们可以确定以4开头的子串前两个字符可以匹配,现在还不能确定是的我们的模式串第三个元素和主串里面刚刚失配的这个元素能不能进行匹配
-
因此我们可以看到我们利用好模式串本身带有的信息,可以跳过中间好几个没有必要进行的对比的子串,可以使算法的效率得到提升
-
利用好模式串本身隐含的一些信息,通过这些信息来确定模式串某一个元素发生失配的时候我们接下来应该怎么处理,显然我们依赖的这个结论只和模式串有关,和我们匹配的是主串的哪个位置没有任何关系
-
如果其他的位置不匹配呢?
-
如果第五个元素不匹配,那么前边的四个元素的信息我们是可以确定的,和模式串保持一致
-
由于前面的子串都很明显的不匹配,我们直接检查下一个子串,发现此时这个子串前面的已知信息和我们的模式串是可以匹配得上的,而刚才发生失配的字符到底是什么内容我们不得而知,所以接下来我们可以从5这个位置开始,依次地往后检查就可以了
-
由于前面的子串都很明显的不匹配,我们直接检查下一个子串,发现此时这个子串前面的已知信息和我们的模式串是可以匹配得上的,而刚才发生失配的字符到底是什么内容我们不得而知,所以接下来我们可以从4这个位置开始,依次地往后检查就可以了
-
由于前面的子串都很明显的不匹配,我们直接检查下一个子串,发现此时这个子串前面的已知信息和我们的模式串是可以匹配得上的,而刚才发生失配的字符到底是什么内容我们不得而知,所以接下来我们可以从1这个位置开始,依次地往后检查就可以了
- 任何一个元素失配的时候我们都是让
i
保持不变,然后j
的值等于一个特定的值
- next数组表示的是当我们第 j 个元素发生失配的时候 j 的值应该修改为多少
KMP算法的代码实现
//KMP算法
int IndexKMP(SString S,SString T,int next[]){
int i = 1;
int j = 1;
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 j - T.length;
}else{
return 0;
}
}
朴素模式匹配 v.s. KMP
求 next 数组
具体过程
-
任何模式串都一样,第一个字符不匹配时,只能匹配下一个子串,因此以后遇到
next[1]
都无脑写0
-
任何模式串都一样,第2个字符不匹配时,应该尝试匹配模式串的第1个字符,因此以后遇到
next[2]
都无脑写1
-
各个
next数组
不管是什么模式串,next[1] = 0; next[2] = 1;
-
接下来的next[i]随着模式串的不同而不同,我们可以在不匹配的位置前面划一根美丽的分界线,将模式串一步一步往后退,直到分界线之前“能对上”或者模式串完全跨过分界线为止,此时 j 指向哪里,next数组值就是多少
- 这样我们就得到了
google
模式串的next数组
原理及代码实现
- 每次移动都是移当前元素之前的串的最长公共前后缀,这个长度也是最长公共前缀的下一个元素的索引,从而刚好忽略最长公共前缀
- 移动的距离为子串长度-公共后缀的长度,移动后子串的长度等于公共前后缀的长度
- 注意这里的公共前后缀都是从左往右的
- 如果
最长公共前后缀
长度为n,那么我们就需要把主串的当前位和模式串的第n+1位进行比较
next数组在代码里的实际使用
- 接下来我们把我们所求的
next数组
放到实际的KMP算法里面尝试着使用这个next数组
手算求next数组练习
- 下面来手算练习求
next数组
- 下面这个模式串
aaaab
给你自己练练手
KMP算法的进一步优化
传统的KMP算法
手算求next数组的方法
next[j] = 当前最大公共前后缀长度 + 1
next数组的优化思路——nextval数组
-
主要看next数组所指的这个字符和原本失配的字符是否相等,如果这两个字符不相等的话,那么next数组的值就保持原有的值不变,但是如果next数组匹配的新位置和当前失配的位置两个字符是相同的,那么这种情况下就可以对next数组进行一个优化
-
本质上只是优化了next数组而已,优化成了nextval数组
-
来看这样一个情况
-
当第三个位置不匹配时,表示此时主串的i指针所指向的这个字符和模式串j指针所指向的这个字符时不相等的,那就说明主串的i指针所指向的那个字符一定不是
a
-
因此当第三个字符匹配失败的时候如果我们让模式串的指针j指向1,那么接下来的匹配也一定是失败的,因为1所指向的字符也是
a
,所以此时匹配失败我们应该让j指针指向next[1] -
所以在前面那一步,在第3个位置匹配失败的时候我们应该让j的值直接等于0,也就是把next[3]的值直接改成0,那么下一次匹配就会跳过1这个位置,减少了一次无效比对
-
再来看下一种情况,假如此时是第五个位置发生不匹配,按照之前的逻辑我们会把j指针修改为next[5]也就是2,但是2所指向的字符也是b,也就是此时一定不会匹配成功
- 因此当第五个字符匹配失败的时候如果我们让模式串的指针j指向2,那么接下来的匹配也一定是失败的,因为2所指向的字符也是
b
,所以此时匹配失败我们应该让j指针指向next[2],也就是1的位置 - 所以在前面那一步,在第5个位置匹配失败的时候我们应该让j的值直接等于1,也就是把next[2],那么下一次匹配就会跳过2这个位置,减少了一次无效比对
- 但是如果此时是6这个位置匹配失败,我们只能确定此时i指向的字符不是c,而不能确定此时的字符是不是a或者b,因此不是每个next[j]的值都能被优化
- 主要看next数组所指的这个字符和原本失配的字符是否相等,如果这两个字符不相等的话,那么next数组的值就保持原有的值不变,但是如果next数组匹配的新位置和当前失配的位置两个字符是相同的,那么这种情况下就可以对next数组进行一个优化
- 本质上只是优化了
next数组
而已,优化成了nextval数组
怎么通过next数组求出nextval数组
- 先求出
next数组
- 首先
nextval[1]
的值无脑写0
,然后我们从前往后依次求后面的nextval数组
的值 - 如果当前
next[j]
所指的字符和当前j所指的字符不相等,那么我们就让nextval[j] = next[j]
- 如果当前
next[j]
所指的字符和当前j所指的字符相等,那么我们就让nextval[j] = nextval[next[j]]
优化后的nextval数组对KMP算法效率的提升
优化前:繁琐冗余
优化后:一步到位