定义
串(String)是由零个或多个字符组成的有限序列,又名字符串。一般记为s="a1a2a3......an"(n>=0)。串可以是空串,即没有字符,直接由“ ”表示(“”里面没有空格)
空串:不包含任何字符。
子串:串中任意个连续的字符组成的子序列称为该串的子串。
空格串:由一个或多个空格组成的串称为空格串(空格串不是空串,其长度为串中空格字符的个数)。
串的逻辑结构
串的逻辑结构和线性表极为相似,区别在于串的数据对象限定为字符集。在基本操作上,串和线性表有很大差别。线性表的基本操作主要以单个元素作为操作对象,如查找,插入,删除某个元素等;而串的基本操作通常以字串作为操作对象,如查找,插入,删除一个字串等
串的存储结构
顺序存储结构(静态)
类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列。在串的顺序存储结构中,为每个串变量分配一个固定长度的存储区,即定长数组
#define MAXSIZE 255 //预定于最大串长为255
typedef struct String {
char ch[MAXSIZE]; //每个分量存储一个字符
int length; //串的实际长度
} String;
初始操作
/**
* 初始化串
*/
void initString(String &string) {
for (int i = 0; i < MAXSIZE; ++i) {
string.ch[i] = '\0';
}
string.length = 0;
}
赋值操作
/**
* 赋值操作
*/
void strAssign(String &string, char *str) {
int i, j;
for (i = 0; i < strlen(str); ++i) {
string.ch[i] = str[i];
}
for (j = i; j < string.length; ++j) {
string.ch[i] = '\0';
}
string.length = strlen(str);
}
复制操作
/**
* 复制操作:把串 str 复制到 string
*/
void strCopy(String &string, String str) {
int i, j;
for (i = 0; i < strlen(str.ch); ++i) {
string.ch[i] = str.ch[i];
}
for (j = i; j < strlen(string.ch); ++j) {
string.ch[i] = '\0';
}
string.length = strlen(str.ch);
}
判空操作
/**
* 判空操作
*/
bool strIsEmpty(String string) {
if (string.length == 0) {
return true;
} else {
return false;
}
}
比较操作
/**
* 比较操作:首先比较同位序的字母大小,其次比较字符串的长度大小
*/
int strCompare(String string, String str) {
for (int i = 0; i < string.length && i < str.length; ++i) {
if (string.ch[i] != str.ch[i]) {
return string.ch[i] - str.ch[i];
}
}
return string.length - str.length;
}
截取操作
/**
* 截取操作:用 sub 返回串 string 的第 pos 个字符起长度为 len 的子串
*/
bool subString(String &sub, String string, int pos, int len) {
if (pos + len > string.length) {
return false;
}
for (int i = pos; i < pos + len; ++i) {
sub.ch[i - pos] = string.ch[i];
}
sub.length = len;
return true;
}
连接操作
/**
* 连接操作:把两个串相连
*/
void strConcat(String &sub, String string, String str) {
int i;
for (i = 0; i < string.length; ++i) {
sub.ch[i] = string.ch[i];
}
for (int j = 0; j < str.length; ++j, ++i) {
sub.ch[i] = str.ch[j];
}
sub.length = string.length + str.length;
}
定位操作
/**
* 定位操作:返回子串在主串中的位序
*/
int strIndex(String string, String str) {
int i = 0, n = string.length, m = str.length;
String sub; //用于暂存子串
while (i <= n - m) {
subString(sub, string, i, m);
if (strCompare(sub, str) != 0) {
i++;
} else {
return i;
}
}
return 0;
}
清空操作
/**
* 清空操作
*/
void strClear(String &string) {
for (int i = 0; i < string.length; ++i) {
string.ch[i] = '\0';
}
string.length = 0;
}
顺序存储结构(动态)
动态分配仍然以一组地址连续的存储单元存放串值的字符序列,但它们的存储空间是在程序执行过程中动态分配得到的
typedef struct String {
char *ch;
int length;
} String;
初始操作
/**
* 初始化串
*/
void initString(String &string,int len) {
string.ch = (char*)malloc(len * sizeof(char));
for (int i = 0; i < len; ++i) {
string.ch[i] = '\0';
}
string.length = 0;
}
赋值操作
/**
* 赋值操作
*/
void strAssign(String &string, char *str) {
int i, j;
for (i = 0; i < strlen(str); ++i) {
string.ch[i] = str[i];
}
for (j = i; j < string.length; ++j) {
string.ch[i] = '\0';
}
string.length = strlen(str);
}
复制操作
/**
* 复制操作:把串 str 复制到 string
*/
void strCopy(String &string, String str) {
int i, j;
for (i = 0; i < str.length; ++i) {
string.ch[i] = str.ch[i];
}
for (j = i; j < string.length; ++j) {
string.ch[i] = '\0';
}
string.length = strlen(str.ch);
}
判空操作
/**
* 判空操作
*/
bool strIsEmpty(String string) {
if (string.length == 0) {
return true;
} else {
return false;
}
}
比较操作
/**
* 比较操作
*/
int strCompare(String string, String str) {
for (int i = 0; i < string.length && i < str.length; ++i) {
if (string.ch[i] != str.ch[i]) {
return string.ch[i] - str.ch[i];
}
}
return string.length - str.length;
}
截取操作
/**
* 截取操作:用 sub 返回串 string 的第 pos 个字符起长度为 len 的子串
*/
bool subString(String &sub, String string, int pos, int len) {
if (pos + len > string.length) {
return false;
}
for (int i = pos; i < pos + len; ++i) {
sub.ch[i - pos] = string.ch[i];
}
sub.length = len;
return true;
}
连接操作
/**
* 连接操作:把两个串相连
*/
void strConcat(String &sub, String string, String str) {
int i;
for (i = 0; i < string.length; ++i) {
sub.ch[i] = string.ch[i];
}
for (int j = 0; j < str.length; ++j, ++i) {
sub.ch[i] = str.ch[j];
}
sub.length = string.length + str.length;
}
定位操作
/**
* 定位操作:返回子串在主串中的位序
*/
int strIndex(String string, String str) {
int i = 0, n = string.length, m = str.length;
String sub; //用于暂存子串
while (i <= n - m) {
subString(sub, string, i, m);
if (strCompare(sub, str) != 0) {
i++;
} else {
return i;
}
}
return 0;
}
清空操作
/**
* 清空操作
*/
void strClear(String &string) {
for (int i = 0; i < string.length; ++i) {
string.ch[i] = '\0';
}
string.length = 0;
}
销毁操作
/**
* 销毁操作
*/
bool strDestroy(String &string) {
free(string.ch);
string.length = 0;
}
链式存储结构(了解)
类似于线性表的链式存储结构,也可采用链表方式存储串值。由于串的特殊性(每个元素只有一个字符),在具体实现时,每个结点既可以存放一个字符,也可以存储多个字符。每个结点称为块,整个链表称为块链结构
typedef struct StringNode {
char ch; //一个结点存一个字符 多个结点存多个字符 char ch[x]
struct StringNode *next;
} StringNode, *String;
初始操作
/**
* 初始化串
*/
bool initString(String &string) {
string = (StringNode*)malloc(sizeof(StringNode));
string->ch = '\0';
string->next = NULL;
return true;
}
串的模式匹配
简单模式匹配(暴力算法)
子串的定位操作通常称为串的模式匹配,它求的是子串(常称模式串)在主串中的位置。这里采用静态顺序存储结构,给出一种不依赖于其他串操作的暴力匹配算法
/**
* 暴力模式匹配算法 -1表示匹配失败
*/
int strIndex(String string, String str) {
int i = 0, j = 0;
while (i < string.length && j < str.length) {
if (string.ch[i] == str.ch[j]) {
++i;
++j;
} else {
i = i - j + 1;
j = 0;
}
}
if (j >= str.length) {
return i - str.length;
} else {
return -1;
}
}
算法思想:从主串的第一个字符起,与子串的第一个字符比较,若相等,则继续逐个比较后续字符;若不相等,则从主串的下一个字符起,重新和子串的字符比较;以此类推
缺点:当某些子串与模式串能部分匹配时,主串的扫描指针经常回溯,导致时间开销大
暴力模式匹配算法的最坏时间复杂度为 O(nm) 其中 n 和 m 分别为主串和子串的长度
改进模式匹配(KMP算法)
在KMP算法中,主串的指针是不会回溯的,子串的指针也只会回溯到部分匹配值后
首先了解几个概念
串的前缀:包含第一个字符,且不包含最后一个字符的子串(多个)
串的后缀:包含最后一个字符,且不包含第一个字符的子串(多个)
部分匹配值:字符串的前缀和后缀的最长相等前后缀长度
例如:字符串 "abab"
前缀为:{a,ab,aba}
后缀为:{b,ab,bab}
部分匹配值为最长相等前后缀长度:2
例如:字符串 "abcac"
"a" 的前缀和后缀都为空集,所以部分匹配值为0
"ab" 的前缀和后缀的交集为空,所以部分匹配值为0
"abc" 的前缀和后缀的交集为空,所以部分匹配值为0
"abca" 的前缀和后缀的交集为'a',所以部分匹配值为1
"abcac" 的前缀和后缀的交集为空,所以部分匹配值为0
我们可以根据子串 "abcac" 的部分匹配值 00010 得到表PM
编号 0 1 2 3 4
子串 a b c a c
匹值 0 0 0 1 0
公式:移动位数 = 已经匹配的字符数 - 部分匹配值
主串的指针为 i 从0开始
子串的指针为 j 从0开始
主串 a b a b c a b c a c b a b
子串 a b c a c
第一趟:
主串 a b a b c a b c a c b a b
子串 a b c
在 i = 2 时不匹配,此时:
已经匹配的字符数 = 2
部分匹配值为 = 0
移动位数 = 2 - 0 = 2
第二趟:
主串 a b a b c a b c a c b a b
子串 a b c a c
在 i = 6 时不匹配,此时:
已经匹配的字符数 = 4
部分匹配值为 = 1
移动位数 = 4 - 1 = 3
第二趟:
主串 a b a b c a b c a c b a b
子串 a b c a c
此时已经匹配
我们已经知道
公式:移动位数 = 已经匹配的字符数 - 部分匹配值
move = j - PM[j - 1]
使用部分匹配值,每当匹配失败时,就会去找它前一个元素的部分匹配值,这样使用起来有些不方便,所以可以将PM表往右移动以为,得到next数组
编号 0 1 2 3 4
子串 a b c a c
匹值 -1 0 0 0 1
我们注意到:
1)第一个元素右移后空缺位置用 -1 来填充,因为第一个字符匹配失败,只需要将子串往后移动一位就可以了,不需要计算子串移动的位数
2)最后一个元素溢出被抛弃了,最后一个元素都匹配上了,那么它的部分匹配值也就没有意义了,此时已经结束了
所以可以把公式由
move = j - PM[j - 1] 改为 move = j - next[j]
相当于将子串的比较指针 j 回退到
j = j - move = next[j]
知道了这些:此时就可以写求 next 数组的算法和 KMP 算法了
/**
* 计算next数组
*/
void get_next(String str, int next[]) {
int i = 0, j = -1;
next[0] = -1;
while (i < str.length) {
if (j == -1 || str.ch[i] == str.ch[j]) {
++i;
++j;
next[i] = j;
} else {
j = next[j];
}
}
}
/**
* KMP算法:返回 -1 表示匹配失败
*/
int strIndex_KMP(String string, String str) {
int i = 0, j = 0;
int next[str.length + 1];
get_next(str, next);
while (i < string.length && j < str.length) {
if (j == -1 || string.ch[i] == str.ch[j]) {
++i;
++j;
} else {
j = next[j];
}
}
if (j >= str.length) {
return i - str.length;
} else {
return -1;
}
}
KMP算法改进
我们知道进行 KMP 算法时要先计算 next 数组,但是 next 数组同样会存储不必要的比较的情况出现,比如
主串 a a a b a a a a b
字串 a a a a b
在第四位时 a != b ,此时会进行三次不必要的比较
这时,我们可以优化一下 next 算法
我们知道 字串 a 与 b 不匹配,而不匹配字符的前面又都是字符 a
此时是没有必要再次进行比较的,也就是说 j 指针回溯位置对应的字符要与发生不匹配时对应的字符不一样就可以避免
比如,我们知道 aaaab 的next数组为 -1 0 1 2 3
当第四位不匹配时 next[3] == 2 即 j 指针回溯到 2 位置,而 2 位置的字符同样是 a 没有比较的意义,则继续把 next[2] == 1 即 j 指针回溯到 1 位置,同样 1 位置字符也为 a,继续 next[1] == 0,又 0 位置也为字符 a 则 nextval[3] = -1
算法改进
/**
* 计算nextval数组
*/
void get_nextval(String str, int nextval[]) {
int i = 0, j = -1;
nextval[0] = -1;
while (i < str.length) {
if (j == -1 || str.ch[i] == str.ch[j]) {
++i;
++j;
if (str.ch[i] != str.ch[j]) {
nextval[i] = j;
} else {
nextval[i] = nextval[j];
}
} else {
j = nextval[j];
}
}
}
原文链接:https://blog.csdn.net/qq_40281548/article/details/115792851