系列文章目录
【数据结构】之起飞(四)栈——你学废了吗?(C语言)
文章目录
前言
一、串
1.串的定义和基本操作
①定义
串,即字符串string,为零个或多个字符组成的有限序列,记为:S’a1a2…an’ (n>=0);
S为串名;
单引号括起来的部分为串的值;(有些语言用双引号,如C)
ai可以是字母、数字或其他字符;(空格也算字符)
串的长度:串中字符的个数;
空串:n=0;(只算引号里面的部分)
子串:串中任意个连续字符组成的子序列;
主串:包含子串的串;
字符在主串中的位置:字符在串中的序号,从1开始;
子串在主串中的位置:子串的第一个字符在主串中的位置;
空串VS空格串:空串长度为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释放空间
不同方案优缺点:
②基于顺序存储实现基本操作
有些操作容易写出,我只列举部分基本操作:
#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;
typedef struct StringNode {
char ch[4]; //每个结点存多个字符,一个指针
struct StringNode* next;
}StringNode, *String;
二、关于串的算法
1.串的朴素模式匹配算法
int index(SString S, SString T)
{
int k = 1;
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;
}
2.KMP算法
①KMP算法
先来看看朴素模式匹配算法的缺点:当某些子串与模式串能部分匹配时,主串的扫描指针i经常回溯,导致时间开销增大。
那么问题来了,模式串指针应该回溯到哪呢?
如果模式串j=k时才发现匹配失败,说明1~k-1 都匹配成功。
在这种算法中,我们需要一个数组来记录状态,并追寻回溯点,我们在数组中需要添加表明状态的数字。
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数组呢?
下面给出代码求解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)已经有了较大提升。
②KMP算法优化
上面我们已经对朴素模式匹配算法进行了优化,而这个优化处理只有当有匹配时模式串能经常性的与部分子串匹配时,才能体现出来,否则KMP算法和朴素模式匹配算法的差别不大。
//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];
}