前言

由于计算机的发展,人们发现对于非数值类型的处理越来越多,进而衍生出来串。

串也就是我们所使用的字符串。

如下所有代码需要做如下引用:

1
2
3
4
5
6
#include <stdio.h>
#include <stdbool.h>
#include <string.h>

//如果你使用的是Visual Studio,则还需要添加如下一条引用
#define _CRT_SECURE_NO_WARNINGS

关于上述引用的说明,原因参考关于C语言的一些零碎思考

串的定义和基本操作

串的定义

串,即字符串(String)是由零个或者多个字符组成的有限序列,一般记作S='a1,a2,...an'($n \geqslant 0 $)。

其中S是串的名称,单引号括起来的字符序列是串的值,其中串字符的个数称为串的长度。当 $n =0$ 的时候称为空串。代码示例:

1
S = "Hello,World"

子串:串中任意连续个字符组成的子序列,例如:上面的子串Hello

串的数据对象限定为:字符集(如中文字符,英文字符)

串的基本操作

函数 说明
StrAssign() 赋值操作,给字符串赋值
StrCopy() 复制操作,将两个字符串进行复制
StrEmpty() 判断字符串是否为空
StrLength() 获取字符串长度,返回串元素的个数
ClearString() 清空操作,清空串为空串
DestoryString() 销毁串
Concat() 串链接,链接两个字符串返回新串
SubString() 获取子串,根据元素位置和长度返回其子串
Index() 定位子串的位置(第一次出现的)
StrCompare() 串比较操作

串的物理结构

串的存储结构

顺序存储

静态顺序存储

1
2
3
4
5
#define MaxLen 255	//定义串的最大长度为255
typedef struct {
char ch[MaxLen];
int length; //串的长度
}SString;

动态顺序存储

1
2
3
4
typedef struct {
char *ch; //用malloc函数申请一片空间,指针指向头地址
int length; //串的长度
}DString;

链式存储

1
2
3
4
typedef struct stringNode {
char ch; //每个节点存储一个字符
struct stringNode* next;
}StrNode;

由于char类型只占一个字节,而struct stringNode*类型指针占四个字节,所以采用上述链式存储的存储密度很低。如果想要使用链式存储,可以尝试使用如下改进:

1
2
3
4
typedef struct stringNode {
char ch[4]; //每个节点存储一个字符
struct stringNode* next;
}StrNode;

这样每个节点存储四个字节的字符和四个字节的指针。

基本操作的实现

注:本示例基本操作的实现是基于静态顺序的存储结构

截取(获取)子串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//求子串(获取子串)
bool SubString(SString* s, SString* sub, int pos, int len) {
if (pos + len > s->length) {
printf("子串索引超出主串\n");
return false;
}
printf("截取的字符串为:");
for (int i = 0; i < len; i++)
{
sub->ch[i] = s->ch[pos + i];
printf("%c", sub->ch[i]);
}
printf("\n");
sub->length = len; //修改子串的长度
return true;
}

比较字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//比较操作
int StrCompare(SString* s1, SString* s2) {
if (s1->length==0 || s2->length==0)
{
printf("存在空串");
return -99;
}
printf("要匹配的字符串为:");
for (int j = 0; j < s2->length; j++)
{
printf("%c",s2->ch[j]);
}
printf("\n");
for (int i = 0; i < s1->length &&i<s2->length; i++)
{
if (s1->ch[i]!=s2->ch[i])
{
return s1->ch[i] - s2->ch[i];
}
}
return s1->length - s2->length;
}

定位子串的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//定位操作
int Index(SString* s, SString* sub) {
if (s->length == 0 || sub->length == 0)
{
printf("存在空串");
return -99;
}
SString temp;
for (int i = 0; i < s->length; i++)
{
//取子串
SubString(s, &temp, i, sub->length);
if (StrCompare(&temp, sub) == 0) {
printf("子串匹配成功,位置为:%d", i+1);
return i+1;
}
}
printf("未匹配到字符串");
return 0;
}

测试代码

1
2
3
4
5
int main() {
SString s = { { 'h','e', 'l', 'l', 'o' },5 };
SString sub = { { 'l', 'o' },2 };
Index(&s, &sub);
}

运行结果:

image-20220813171339807

朴素模式匹配算法

通俗来说就是上面所实现的字符串的操作的“定位子串的位置”,俗称“暴力解”。

现在来通过数组下标来实现朴素模式匹配算法,也就是上面的定位操作,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//朴素模式匹配算法——利用数组下标
int NaiveMatch(char* s[],char* m[]) {
int i = 0; //模式串下标指针
char temp[MaxLen]; //临时字符串
for (int j = 0; j < (strlen(s)-strlen(m)+1); j++)
{
SubString(s, temp, i, strlen(m)); //获取指定长度字符串存储到临时字符串中
//比较截取的字符串和模式串
if (StrCompare(temp, s)==0)
{
i++; //模式串指针+1
}
else
{
i = 0;
j = j - i + 2;
}
//如果模式串指针超过了模式串的长度,意味着匹配到了合适的字符串
if (i>strlen(m))
{
printf("已匹配到字符串,位置:%d", strlen(s) - strlen(m)+1);
return strlen(s) - strlen(m)+1;
}
}
printf("匹配失败");
return -1;
}

//测试代码
int main() {
char s[MaxLen] = { 'h','e', 'l', 'l', 'o' };
char sub[MaxLen] = { 'l', 'o' };
NaiveMatch(&s, &sub);
}

运行结果:

image-20220817021229084

KMP算法

KMP算法简介

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)

如上来源:百度百科kmp算法

KMP算法是在上面的朴素模式匹配算法的基础上改进而来

KMP算法实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//求Next数组
bool GetNext(char* m[],char* next[]) {
int i, k;
i = 1;
k = 0;
next[1] = 0;
while (i<strlen(m))
{
if (k==0 || m[i]==m[k])
{
++i;
++k;
next[i] = k;
}
else
{
k = next[k];
}
}
}

改进KMP算法

不想写,欠……

这部分内容有点繁杂,我在尝试使用更加直观的方法来说明。如上也同理,仅贴代码示例;

image-20220818020541675

End

救命越来越不想写了,属实太难用文字解释了,如果用视频的话要比文字快很多。