目录:
- 串:
- 5.1 定义:
- 5.2 抽象数据类型:
- 5.3 基本功能实现:
- 5.3.1 StrAssign(T,*chars):
- 5.3.2 StrCopy(T,S):
- 5.3.3 ClearString(S):
- 5.3.4 StringEmpty(S):
- 5.3.5 StrLength(S):
- 5.3.6 StrCompare(S,T):
- 5.3.7 Concat(T,S1,S2):
- 5.3.8 SubString(Sub,S,pos,len):
- 5.3.9 Index(S,T,pos):
- 5.3.10 Replace(S,T,V):
- 5.3.11 StrInsert(S,pos,T):
- 5.3.12 StrDelete(S,pos,len):
- 5.3.13 StrPrint(String T):
- 5.4 串的存储结构:
- 5.5 串的模式匹配:
串:
5.1 定义:
串(string) 是由零个或多个字符组成的有限序列,又名叫字符串。
- 串中的字符数目N称为串的长度
- 零个字符的串称为空串(null string)
- 空格串:只包含空格的串,内容是有长度的,可以不止一个空格。
- 子串与主串:串中任意个数的连续字符组成的子序列称为该串的子串,相应的,包含子串的串称为主串。
- 子串在主串中的位置就是子串的第一个字符在主串中的序号。
5.2 抽象数据类型:
ADT 串(string)
Data
串中元素仅由一个字符组成,相邻元素具有前驱和后继的关系。
Operation
StrAssign(T,*chars):生成一个值等于字符串常量chars的串T。
StrCopy(T,S):串S存在,由串S复制得串T。
ClearString(S):串S存在,将串清空。
StringEmpty(S):若S为空,返回true,否则返回false
StrLength(S):返回串S中的元素个数,即串的长度。
StrCompare(S,T):若S>T,返回值>0,若S=T,返回0,若S<T,返回值<0。
Concat(T,S1,S2):用T返回由S1和S2联接而成的新串。
SubString(Sub,S,pos,len):串S存在,用Sub返回串S的第pos个字符起长度为len的子串
Index(S,T,pos):串S和T存在,T为非空串,若主串S中存在和串T值相同的子串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则返回0。
Replace(S,T,V):串S,T,V存在,T是非空串。用V替换主串S中出现的所有与T相等的不重叠的子串。
StrInsert(S,pos,T):串S和T存在,在串S的第pos个字符之前插入串T。
StrDelete(S,pos,len):串S存在,从串S中删除第pos个字符起长度为len的子串。
endADT
5.3 基本功能实现:
5.3.1 StrAssign(T,*chars):
生成一个值等于字符串常量chars的串T。
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 40 /* 存储空间初始分配量 */
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType; /* ElemType类型根据实际情况而定,这里假设为int */
typedef char String[MAXSIZE + 1]; /* 0号单元存放串的长度 */
/* 生成一个其值等于chars的串T */
Status StrAssign(String T, char* chars)
{
int i;
if (strlen(chars) > MAXSIZE) //若超过字符串的最大长度
return ERROR;
else
{
T[0] = strlen(chars); //用字符串的第一位存储字符串的长度
for (i = 1; i <= T[0]; i++)
T[i] = *(chars + i - 1);
return OK;
}
}
5.3.2 StrCopy(T,S):
串S存在,由串S复制得串T。
/* 由串S复制得串T */
Status StrCopy(String T, String S)
{
int i;
for (i = 0; i <= S[0]; i++)
T[i] = S[i];
return OK;
}
5.3.3 ClearString(S):
串S存在,将串清空。
/* 初始条件:串S存在。操作结果:将S清为空串 */
Status ClearString(String S)
{
S[0] = 0;/* 令串长为零 */
return OK;
}
5.3.4 StringEmpty(S):
若S为空,返回true,否则返回false。
/* 若S为空串,则返回TRUE,否则返回FALSE */
Status StrEmpty(String S)
{
if (S[0] == 0)
return TRUE;
else
return FALSE;
}
5.3.5 StrLength(S):
返回串S中的元素个数,即串的长度。
/* 返回串的元素个数 */
int StrLength(String S)
{
return S[0];
}
5.3.6 StrCompare(S,T):
若S>T,返回值>0,若S=T,返回0,若S<T,返回值<0。
/* 初始条件: 串S和T存在 */
/* 操作结果: 若S>T,则返回值>0;若S=T,则返回值=0;若S<T,则返回值<0 */
int StrCompare(String S, String T)
{
int i;
for (i = 1; i <= S[0] && i <= T[0]; ++i)
if (S[i] != T[i])
return S[i] - T[i];
return S[0] - T[0];
}
5.3.7 Concat(T,S1,S2):
用T返回由S1和S2联接而成的新串。
/* 用T返回S1和S2联接而成的新串。若未截断,则返回TRUE,否则FALSE */
Status Concat(String T, String S1, String S2)
{
int i;
if (S1[0] + S2[0] <= MAXSIZE)
{ /* 未截断 */
for (i = 1; i <= S1[0]; i++)
T[i] = S1[i];
for (i = 1; i <= S2[0]; i++)
T[S1[0] + i] = S2[i];
T[0] = S1[0] + S2[0];
return TRUE;
}
else
{ /* 截断S2 */
for (i = 1; i <= S1[0]; i++)
T[i] = S1[i];
for (i = 1; i <= MAXSIZE - S1[0]; i++)
T[S1[0] + i] = S2[i];
T[0] = MAXSIZE;
return FALSE;
}
}
5.3.8 SubString(Sub,S,pos,len):
串S存在,用Sub返回串S的第pos个字符起长度为len的子串。
/* 用Sub返回串S的第pos个字符起长度为len的子串。 */
Status SubString(String Sub, String S, int pos, int len)
{
int i;
if (pos<1 || pos>S[0] || len<0 || len>S[0] - pos + 1)
return ERROR;
for (i = 1; i <= len; i++)
Sub[i] = S[pos + i - 1];
Sub[0] = len;
return OK;
}
5.3.9 Index(S,T,pos):
串S和T存在,T为非空串,若主串S中存在和串T值相同的子串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则返回0。
/* 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0。 */
/* 其中,T非空,1≤pos≤StrLength(S)。 */
int Index(String S, String T, int pos)
{
int i = pos; /* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */
int j = 1; /* j用于子串T中当前位置下标值 */
while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */
{
if (S[i] == T[j]) /* 两字母相等则继续 */
{
++i;
++j;
}
else /* 指针后退重新开始匹配 */
{
i = i - j + 2; /* i退回到上次匹配首位的下一位 */
j = 1; /* j退回到子串T的首位 */
}
}
if (j > T[0])
return i - T[0];
else
return 0;
}
/* T为非空串。若主串S中第pos个字符之后存在与T相等的子串, */
/* 则返回第一个这样的子串在S中的位置,否则返回0 */
int Index2(String S, String T, int pos)
{
int n, m, i;
String sub;
if (pos > 0)
{
n = StrLength(S); /* 得到主串S的长度 */
m = StrLength(T); /* 得到子串T的长度 */
i = pos;
while (i <= n - m + 1)
{
SubString(sub, S, i, m); /* 取主串中第i个位置长度与T相等的子串给sub */
if (StrCompare(sub, T) != 0) /* 如果两串不相等 */
++i;
else /* 如果两串相等 */
return i; /* 则返回i值 */
}
}
return 0; /* 若无子串与T相等,返回0 */
}
5.3.10 Replace(S,T,V):
串S,T,V存在,T是非空串。用V替换主串S中出现的所有与T相等的不重叠的子串。
/* 初始条件: 串S,T和V存在,T是非空串(此函数与串的存储结构无关) */
/* 操作结果: 用V替换主串S中出现的所有与T相等的不重叠的子串 */
Status Replace(String S, String T, String V)
{
int i = 1; /* 从串S的第一个字符起查找串T */
if (StrEmpty(T)) /* T是空串 */
return ERROR;
do
{
i = Index(S, T, i); /* 结果i为从上一个i之后找到的子串T的位置 */
if (i) /* 串S中存在串T */
{
StrDelete(S, i, StrLength(T)); /* 删除该串T */
StrInsert(S, i, V); /* 在原串T的位置插入串V */
i += StrLength(V); /* 在插入的串V后面继续查找串T */
}
} while (i);
return OK;
}
5.3.11 StrInsert(S,pos,T):
串S和T存在,在串S的第pos个字符之前插入串T。
/* 初始条件: 串S和T存在,1≤pos≤StrLength(S)+1 */
/* 操作结果: 在串S的第pos个字符之前插入串T。完全插入返回TRUE,部分插入返回FALSE */
Status StrInsert(String S, int pos, String T)
{
int i;
if (pos<1 || pos>S[0] + 1)
return ERROR;
if (S[0] + T[0] <= MAXSIZE)
{ /* 完全插入 */
for (i = S[0]; i >= pos; i--)
S[i + T[0]] = S[i];
for (i = pos; i < pos + T[0]; i++)
S[i] = T[i - pos + 1];
S[0] = S[0] + T[0];
return TRUE;
}
else
{ /* 部分插入 */
for (i = MAXSIZE; i <= pos; i--)
S[i] = S[i - T[0]];
for (i = pos; i < pos + T[0]; i++)
S[i] = T[i - pos + 1];
S[0] = MAXSIZE;
return FALSE;
}
}
5.3.12 StrDelete(S,pos,len):
串S存在,从串S中删除第pos个字符起长度为len的子串。
/* 初始条件: 串S存在,1≤pos≤StrLength(S)-len+1 */
/* 操作结果: 从串S中删除第pos个字符起长度为len的子串 */
Status StrDelete(String S, int pos, int len)
{
int i;
if (pos<1 || pos>S[0] - len + 1 || len < 0)
return ERROR;
for (i = pos + len; i <= S[0]; i++)
S[i - len] = S[i];
S[0] -= len;
return OK;
}
5.3.13 StrPrint(String T):
输出字符串T。
/* 输出字符串T */
void StrPrint(String T)
{
int i;
for (i = 1; i <= T[0]; i++)
printf("%c", T[i]);
printf("\n");
}
5.4 串的存储结构:
串的存储结构与线性表相同,分为顺序存储和链式存储两种。
5.4.1 串的顺序存储结构:
串的顺序存储结构是用一组地址连续地存储单元来存储串中的字符序列的。按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区。一般是用定长数组来定义。
一般可以将串的长度值保存在数组的0下标位置处。但也可以在串值后面加一个不计入串长度的结束标记字符,比如\0表示串的终结。通过遍历计算获得串的长度。
结构体:
#define MAXLEN 255 //预定义最大串长为255
typedef struct{
char ch[MAXLEN]; //静态数组实现(定长顺序存储)
//每个分量存储一个字符
//每个char字符占1B
int length; //串的实际长度
}SString;
基本操作:
#define MAXLEN 255
typedef struct{
char ch[MAXLEN];
int length;
}SString;
// 1. 求子串
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.cn[i-pos+1] = S.ch[i];
Sub.length = len;
return true;
}
// 2. 比较两个串的大小
int StrCompare(SString S, SString T){
for (int i; 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;
}
// 3. 定位操作
int Index(SString S, SString T){
int i=1;
n = StrLength(S);
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相等的子串
}
5.4.2 串的链式存储结构:
相比于线性表,一个结点可以存放一个字符,也可以存放多个字符,最后一个结点若是未被占满时,可以用#或其他字符补全。
结构体:
typedef struct StringNode{
char ch; //每个结点存1个字符
struct StringNode *next;
}StringNode, * String;
5.5 串的模式匹配:
5.5.1 朴素的模式匹配算法:
从一个主串中寻找到子串的位置,这种子串的定位操作通常被称为串的模式匹配。
我们对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做小循环,指导匹配成功或全部遍历完成为止。
我们设置串的长度位于数组的0下标位置处。
/* 朴素的模式匹配法 */
int Index(String S, String T, int pos)
{
int i = pos; /* i用于主串S中当前位置下标值,若pos不为1,则从pos位置开始匹配 */
int j = 1; /* j用于子串T中当前位置下标值 */
while (i <= S[0] && j <= T[0]) /* 若i小于S的长度并且j小于T的长度时,循环继续 */
{
if (S[i] == T[j]) /* 两字母相等则继续 */
{
++i;
++j;
}
else /* 指针后退重新开始匹配 */
{
i = i - j + 2; /* i退回到上次匹配首位的下一位 */
j = 1; /* j退回到子串T的首位 */
}
}
if (j > T[0])
return i - T[0];
else
return 0;
}
这种方法我们称为朴素的模式匹配算法,比较繁琐低效。
5.5.2 KMP模式匹配算法:
KMP模式匹配算法可以大大避免重复遍历的情况。
KMP的主要思想是:当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。这里就会运用到一个前缀表next数组。
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。
前缀表:记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。
构造next数组:
5.5.2.1 前缀统一减一:
void getNext(int* next, const string& s){
int j = -1;
next[0] = j;
for(int i = 1; i < s.size(); i++) { // 注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
此时如果输入的模式串为aabaaf,对应的next为-1 0 -1 0 1 -1。
进行匹配:
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = -1; // // 因为next数组里记录的起始位置为-1
for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
return (i - needle.size() + 1);
}
}
return -1;
}
};
5.5.2.2前缀表不减一:
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作
j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了
}
if (s[i] == s[j]) {
j++;
}
next[i] = j;
}
}
此时如果输入的模式串为aabaaf,对应的next为 0 1 0 1 2 0
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.size(); i++) {
while(j > 0 && haystack[i] != needle[j]) {
j = next[j - 1];
}
if (haystack[i] == needle[j]) {
j++;
}
if (j == needle.size() ) {
return (i - needle.size() + 1);
}
}
return -1;
}
};
该部分KMP算法相关内容参考了代码随想录的b站视频:
帮你把KMP算法学个通透!(求next数组代码篇)_哔哩哔哩_bilibili
利用KMP算法,可以大幅减少时间复杂度,避免重复匹配。
本文为自己在学习数据结构过程中所记录整理的一些笔记,后续将持续更新后面的内容。
如果这篇文章对你有帮助,可以给我点一个小小的赞。
如果你对我的栏目有兴趣,也欢迎关注我,一同学习。
主要参考:《大话数据结构》——程杰