💖少年没有乌托邦,心有远方自明朗💖
系列文章目录
【数据结构】之起飞 (一)什么是数据结构及其基本概念(C语言)
【数据结构】之起飞(二)算法的时间复杂度及空间复杂度(C语言)
【数据结构】之起飞(三)线性表 ——看这篇就够了(C语言)
【数据结构】之起飞(四)栈——你学废了吗?(C语言)
【数据结构】之起飞(五)队列 ——你还记得否?(C语言)
文章目录
前言
大家好,我是小沐!😃编程路上一个人可能走的更快,但一群人才能走得更远,关注小沐一起学习不迷路!今天分享的是数据结构串的基本操作及朴素模式匹配算法和KMP算法的介绍,话不多说,秃头走起——>冲冲冲!!!
一、串
1.串的定义和基本操作
①定义
串,即字符串string,为零个或多个字符组成的有限序列,记为:S=‘a1a2…an’ (n>=0);
S为串名;
单引号括起来的部分为串的值;(有些语言用双引号,如C)
ai可以是字母、数字或其他字符;(空格也算字符)
串的长度:串中字符的个数;
空串:n=0;(只算引号里面的部分)
子串:串中任意个连续字符组成的子序列;
主串:包含子串的串;
字符在主串中的位置:字符在串中的序号,从1开始;
子串在主串中的位置:子串的第一个字符在主串中的位置;
空串VS空格串:空串长度为0,空格串是有长度的且一个空格占据一个字节的空间
其实,串是一种特殊的线性表,数据元素之间为线性关系,数据对象为字符集而不仅仅是单个字符,所以串的基本操作通常以子串为操作对象。
②基本操作
假设有串T="", S=“Hello,World!”, W=“You”
StrAssign(&T,chars):赋值,把串T赋值为chars
StrCopy(&T,S):复制,由串s复制得到串T
StrEmpty(S):判空
StrLength(S):求串长
CleanString(&S):清空串
Destroystring(&S):销毁串 (回收内存空间,不同于清空)
Concat(&T,S1,S2):串联接,用T返回S1和S2联接成的新串(存储空间可能需要拓展)
SubString(&Sub,S,pos,len):求子串,用Sub返回串S的第pos个字符起长度为len的子串。
Index(S,T):定位,若主串S中存在与串T相同的子串,则返回它在主串中第一次出现的位置,否则返回0.
StrCompare(S,T):比较,若S>T,返回值大于0;若S=T,返回值=0;若S<T,返回值<0。
比较原则:
从左到右,先出现更大字符的串更大;
长串的前缀与短串相同时,长串更大;
两串完全相同才相等。
2.串的存储结构
①顺序存储
定长顺序存储(静态数组实现)
#define MAXSIZE 255 //宏定义串最大长度
Typedef struct {
char ch[MAXSIZE];//每个分量存储一个字符
int length; //串长
}SString;
上述方法程序结束后系统回收空间
堆分配存储(动态数组实现)
Typedef struct {
char* ch; //指向串的基地址
int length; //串长
}HString;
HString S;
S.ch = (char*)malloc(MAXSIZE * sizeof(char));
S.length = 0;
上述方法需要手动free释放空间
优缺点:定长顺序存储简单但是容量一定,堆分配存储容量可拓展但是比较麻烦。
②基于顺序存储实现基本操作
有些操作容易写出,我只列举部分基本操作:
注意:为方便操作我在0处未存储数据,这个大家根据自己的习惯即可。
#define MAXSIZE 255 //宏定义串最大长度
Typedef struct {
char ch[MAXSIZE];//每个分量存储一个字符
int length; //串长
}SString;
//为方便以下操作数据从下标为1处开始存起,舍弃0处
//求子串
//用Sub返回串S的第pos个字符起长度为len的子串
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]; //数据从下标为1处开始存起,舍弃0处
}
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 <= T.length; i++)
{
if (S.ch[i] != T.ch[i])
return S.ch[i] - T.ch[i];
}
//若前面部分都相同
return S.length - T.length;
}
//定位操作(可用求子串+比较)
//若主串S中存在与串T相同的子串,则返回它在主串中第一次出现的位置,否则返回0
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;
}
③链式存储
方案一:
typedef struct StringNode {
char ch; //每个结点存一个字符
struct StringNode* next;
}StringNode, *String;
这种方法存储密度低,每个结点存一个字符一个指针,这意味着我们在存一个1B的字符时需要花费8B的空间存取对应的指针变量。(64位操作系统指针变量为8字节,32位为4字节。
方案二:
typedef struct StringNode {
char ch[4]; //每个结点存多个字符,一个指针
struct StringNode* next;
}StringNode, *String;
这种方案存储密度高,每个结点存取多个字符和一个指针,当结点存不满时可用符号#或\0填充。
二、关于串的算法
1.串的朴素模式匹配算法
小伙伴可能会问:什么是模式匹配?
答:串的模式匹配,是在主串中找到与模式串相同的子串,并返回其所在位置。
其实就是基本操作中的定位操作,上文中我们使用基本操作求子串和比较的思路实现了定位操作即模式匹配,接下来我们直接用数组实现。
//若主串S中存在与串T相同的子串,则返回它在主串中第一次出现的位置,否则返回0
int index(SString S, SString T)
{
int k = 1;//主串S字符下标
int i = k;
int 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) //j导致结束循环,说明已找到模式串
return k;
else //i导师结束循环,说明未找到
return 0;
}
也可以不用k变量而利用i和j的关系:**i=i-j+2 **来实现
算法的时间复杂度:
若模式串长度为m,主串长度为n,则
匹配成功的最好时间复杂度为O(m);
匹配失败的最好时间复杂度为O(n-m+1)=O(n-m),若m<<n,则为O(n)
匹配成功/失败的最坏时间复杂度为O(m*(n-m+1)=O(nm)
2.KMP算法
①KMP算法
KMP算法实际上为朴素模式匹配算法的优化,由D.E.Knuth, J.H.Morris和V.R.Pratt三人提出,因此叫KMP算法。
先来看看朴素模式匹配算法的缺点:当某些子串与模式串能部分匹配时,主串的扫描指针i经常回溯,导致时间开销增大。
改进思路:主串指针不回溯,只有模式串指针回溯 j = next [ j ]
那么问题来了,模式串指针应该回溯到哪呢?
能否直接回溯到j=1?
答:有时候可以的,但是有时候是不行的。
如果在j=5匹配失败的话直接回到j=1会掩盖掉主串中与j=4相对应的g,因此这个时候j应该回溯到j=2处。
如果模式串j=k时才发现匹配失败,说明1~k-1 都匹配成功。
在这种算法中,我们需要一个数组next来记录状态,并追寻回溯点,我们在数组中需要添加表明状态的数字。
int Index_KMP(SString S, SString T, int next[])
{
int i = 1;
int j = 1;
while (i <= S.length && j <= T.lenghth)
{
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;
}
上述方法我们已经实现了,还有一个问题,我们如何求模式串的next数组呢?
手算思路如下:
串的前缀:包含第一个字符,且不包含最后一个字符的子串
串的后缀:包含最后一个字符,且不包含第一个字符的子串
当第j个字符匹配失败,由前1~j-1个字符组成的串记为S,则:
next[j]= S的最长相等前后缀长度+1,特别的next[1]=0. next[2]=1.
下面给出代码求解next数组:
//求模式串T的next数组
void get_next(SString T, int next[])
{
int i = 1;
int j = 0;
next[1] = 0;
while (i < T.length)
{
if (j == 0 || T.ch[i] == T.ch[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
}
优化后的朴素模式匹配算法即KMP算法的时间复杂度为O(m+n),相比于朴素模式匹配算法的O(mn)已经有了较大提升。
总代码为:
void get_next(SString T, int next[])
{
int i = 1;
int j = 0;
next[1] = 0;
while (i < T.length)
{
if (j == 0 || T.ch[i] == T.ch[j])
{
++i;
++j;
next[i] = j;
}
else
j = next[j];
}
}
int Index_KMP(SString S, SString T, int next[])
{
int i = 1;
int j = 1;
get_next( T, next);
while (i <= S.length && j <= T.lenghth)
{
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算法和朴素模式匹配算法的差别不大。
对于google这个字符串,若扫描到第二个g发现不匹配,则next数组会回溯到1号位,但其实1号位也是g,肯定也是不匹配的,浪费了一次匹配时间。所以我们实际可以让第二个g不匹配时回溯到和第一个g不匹配时一样的状态,即0号位置,并用nextval数组记录。
总之,如果这一轮匹配失败,相同字符的nextval值和第一次出现时相同,不同字符的和next数组一致,不改变。
这样就减少了对一样字符的重复比对,减少了很多匹配次数。
KMP算法优化:
**当子串和模式串不匹配时, j=nextval [ j ] **
//nextval数组的求法:
//先算出next数组;
//令nextval[1]=0;
for (int j = 2; j <= T.length; j++0)
{
if (T.ch[next[j]] == T.ch[j])
nextval[j] = nextval[next[j]];
else
nextval[j] = next[j];
}
总结
今日分享到此结束,由于笔者还在求学之路上辗转徘徊,水平有限,文章中可能有不对之处,还请各位大佬指正,祝愿每一个热爱编程的人都能实现追求,考研上岸进大厂,熬夜秃头卷中王。最后欢迎关注小沐,学习路上不迷路!😜