目录
01-04-串匹配简单模式(Brute-Force,布鲁特-福斯)算法
计算机处理对象分为数值数据和非数值数据,字符串是最基本的非数值数据。字符串处理在语言编辑,信息检索,文字编辑等问题上有广泛应用。字符串也是一种特定的线性表,其特殊性在于组成线性表的每个元素就是一个单字符。
一,串的基础概念
字符串(String)是由零个或多个字符组成的有限序列。记为S="a1 a2 ……an"(n>=0)
其中,S是串的名字,而引号里面的是串的值,每个ai可以是字母,数字或者其他字符。n是串中字符的个数,称为串的长度,n=0时的串称为空串(Null String)。
这里需要注意,前后的引号是界限符,并不属于串。
子串:串中任意个连续的字符组成的子序列称为该串的子串。
主串:包含子串的串称为主串。子串就是主串的子集
子串在主串中的位置:通常将字符在串中的序号称为该字符在串中的位置。子串在主串的位置值就是子串第一个字符在主串中的位置。
现有一个串A=“Hello Word ",B="Hello",C="Word",可以看出B与C都是A的子串,其中B在主串A的位置就是‘H'的位置,1;而C在A中的位置就是'W'的位置,7。(空格也是字符哦)
串相等:当且仅当两个串的值相等时,称这两个串是相等的,也就是两串长度相同,其每个对应位置字符也相同。
再次强调空格也是字符,空格也是字符,空格也是字符。“ ”与“”可不相同,虽说都看不见字符,但第一个有空格,是由空格组成的空格串,而第二个就是无任何字符的空串。
串就是特定的线性表,串的逻辑结构与线性表极为相似,其特定性仅在于串的数据对象限定为字符集。
- 抽象数据类型ADT的设计
ADT String {
数据元素:字符类型
结构关系:字符串中元素相邻,共同组成字符串
基本操作:
- init_String()
操作前提:堆区存有足够大空间
操作结果:返回一个指向size大小的char类型数组的字符串指针*String
- Push_String(S, char)
操作前提:串S存在,char是字符类型变量
操作结果:把串S的第len+1位置的值变为char,并且将len加一
- Pop_String(S)
操作前提:串S存在,并且其不为空
操作结果:将串S中长度len减一
- Erase_string(S, chars)
操作前提: 串S存在,chars是字符指针类型变量
操作结果:删除字符串S中与chars相同的子串,并将空余位置压缩
- Reper_string(S, chars)
操作前提: 串S存在,chars是字符指针类型变量
操作结果:记录并返回字符串S中与chars相同的子串数量
- Out_String(S)
操作前提:串S存在,并且其不为空
操作结果:将串S中长度字符逐个输出到屏幕
} ADT String;
二,串的存储实现
01.定长顺序串
定长顺序串是将串设计成一种静态结构类型,串的存储分配是在编译时完成的。这与线性表的顺序存储结构相似。使用一段连续的存储单元存储串的字符序列。就是将顺序表存储的元素固定成了char字符。
其定义如下:
const int Max = 100; // 首先确定最大容量
typedef struct Static_String // 静态字符串
{
char S[Max]; // 直接在编译前分配连续字符存储空间
int len; // 记录字符串长度
}sstring;
下面进行定长字符串的基本操作实现:
串的基本运算除了与线性表类似的插入,删除,修改运算外,还有在字符串中求子串,以及求子串在主串中的位置等特殊操作。(姑且算是吧,毕竟除了字符串,其他线性表也不这样干呀)
01-01-串插入函数
在进行顺序串的插入时,插入位置pos将串分为两部分(假设为A,B,长度为LA,LB),待插入部分(假设为C,长度LC),则串AB变为ACB,由于是顺序串,插入C会引起B的后移。并且这是静态顺序栈,不能随时扩容,因此会出现以下三种情况:
① 插入后的串长(LA+LB+LC)<=MAXLEN,则只需将B向后移动LC个元素位置,再将C插入;
② 插入后串长>MAXLEN,不过pos+LC<=MAXLEN,则B在后移时需要舍弃部分字符;
③ 插入后串长>MAXLEN并且pos+LC>MAXLEN,则B就放不下了,全部需要舍弃,C也未必能放下,可能需要舍弃。
实现如下:
void StrInsert(sstring* s) {
int pos; // 插入的位置 这里默认在指定位置前插入,然后能插入多少是多少,插不下的就丢了
sstring* new_s=(sstring*)malloc(sizeof(sstring));// 插入的字符串
printf("请输入插入的位置:\n");
scanf("%d", &pos);
// 首先对插入的位置进行判断,看看是否合理
if (pos >= 0 && pos <= s->len) {
// 合理就可以输入了,能插入多少就看空间了
printf("请输入插入的串的长度:\n");
scanf("%d", &new_s->len);
printf("请输入插入的串:\n");
scanf("%s", new_s->S);
int old = new_s->len + pos; // 原字符串结束迁移的位置
if (old > Max-1) { // 塞不下原来位置的,直接摆烂不塞了,相当于置换
for (int k = 0, j = pos; j < Max-1; ++k, ++j) { // 新子串能塞多少是多少,看原串的大小咯
s->S[j] = new_s->S[k];
}
}
else {
// 原字符串迁移后的位置 原字符串开始的位置
for (int n = s->len-1; n >= pos; --n) { // 把新子串以及新子串占据的元素插入后的老元素向后搬,能搬就搬,没位置就算了
if (n + new_s->len < Max) {
s->S[n + new_s->len] = s->S[n];
}
}
// 新串起始位置 新串在原串起始位置 这里新串可以把自己全部插入
for (int k = 0, j = pos ; k < new_s->len; ++k, ++j) {
s->S[j] = new_s->S[k];
}
}
}
else printf("不合理!!!\n");
}
01-02-顺序串删除函数
删除串中内容,需要确定开始删除的下标(这个下标要合法,不能在字符串之外),其次就是需要删除的长度,超过从删除位置开始的元素总个数,否则就会下标越界。
实现如下:
// 删除函数 在s串中,删除从pos起的len个字符
void StrDelete(sstring* s, int pos, int len) {
if (pos<0 || pos>(s->len-len)) return; // 参数不合法 需要注意的是删除的长度不能超过从删除位置开始的元素总个数,否则就会下标越界
for (int i = pos + len; i < s->len; ++i) { // 这个就是单纯挪动字符,不必考虑是否舍弃这麻烦事
s->S[i - len] = s->S[i];
}
s->len = s->len - len;
}
01-03-串比较函数
这里只需要明确字符串比较规则:
从两个字符串的第一个字符开始逐个进行比较(按字符的ASCII值进行大小比较),直到出现不同的字符或遇到‘\0’为止。
如果全部字符都相同,就认为两字符串相等,返回0;
若出现了不相同的字符,则以第一个不相同的字符比较结果为准,若前者字符大于后者,则返回1,否则返回-1.
所以只需要遍历依次比较每个对应字符的ASCLL值即可,为了方便,我们将正数看作1,负数看作-1;
实现如下:
// 串比较函数 等于 返回0 ,大于返回正数,小于返回负数
int StrCompare(sstring a, sstring b) {
for (int i = 0; i < a.len; ++i) {
if (a.S[i] != b.S[i]) return a.S[i] - b.S[i]; // 如果字符串长度不同,就比较两个第一个不同的字符所对应ASCLL值大小
}
return (a.len - b.len); // 如果前面都相同,那么谁的长度长,谁就大
}
01-04-串匹配简单模式(Brute-Force,布鲁特-福斯)算法
思路:简单的模式匹配算法是一种带有回溯的匹配算法,算法的基本思想是:从主串S的第pos个字符开始,和模式串T的第一个字符开始比较,如果相同,就继续比较后续字符,如果不同,回溯到主串的pos+1位置,也就是上次开始比较的地方向后移动一位,又开始从模式串T的第一个字符开始比较,直到T中的字符连续出现在S中,就是匹配成功了,返回和T中第一个字符相等的字符的位置,就是定位了,当然也可以将整个S串遍历完,统计T串出现的次数;不过如果遍历完S都没有与T完全匹配的子串,就称作匹配不成功。
实现这个操作的核心就是匹配失败后如何回溯到上个起点的下一个位置。这里可以使用一个变量用来记录指示上个起点位置,再用一个变量记录当前程序遍历S的位置,用来逐步比较。
这里用此算法实现指定字符串在主串中出现次数的统计:
// 统计次数
int Reper_string(string* L, char* a) {
int n = 0;
int left = 0, right = 0, pta = 0;
for (int i = 0; i < L->len; ++i) {
if (a[pta]) { // a 非空
if (L->S[i] == a[pta]) { // 同走
++pta;
++right;
}
else { // 异出
++left;
right = left;
pta = 0;
i = left - 1;
}
}
else { // a 完 n++
++n;
pta = 0;
left = right;
i = left - 1;
}
}
printf("ALL: %d\n", n);
return n;
}
不过这个算法在思路上简单,但时间复杂度较高,运气不好直接O(S.len*T.len),悲;
想要降低时间复杂度可以采用无回溯的KMP算法。(后续扩充,滑稽,流汗,喜)
02.堆串
堆串嘛,顾名思义,在堆区实现串的存储。
字符串包括名与串值两部分,串名用符号表存储,串值就是采用了堆串存储方法存储。
符号表
在计算机科学中,符号表是一种用于语言翻译器(例如编译器和解释器)中的数据结构。在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。
串名符号表:
所有串名的存储映像构成一个符号表。借助此结构可以在串名与串值之间建立一个对应关系,称为串名的存储映像。
堆串存储方法:
以一组地址连续的存储单元顺序存放串中的字符,但它们的存储空间是在程序执行中1动态分配的。系统将一个地址连续,容量很大的存储空间作为字符串的可用空间,每当建立一个新串时,系统就从这个空间分配出一个大小和字符串长度相同的空间用于存储新串的串值。
这里用a=“Hello Word",b="Hi! String",c="process"为例,演示一波,此时free=23;
堆串存储表示:可以直接利用C语言的堆区,使用函数malloc()和函数free()完成动态存储管理。 定义如下:
typedef struct String_Arr
{
// int size;
int len;
char* S;
}string;
其中len域指示串的长度,S域指示串的起始地址,size域存储串的实际存放字符的数量,这里可以不需要size,每次按照上述定义给与新字符串长度相同的空间存储新字符串。不过我想在每个字符串中实现与定长顺序串一样的存储,只在原字符串长度不足时进行扩容。
下面以标准形式为准,实现堆串的基本操作。由于这种类型的串变量中串值的存储位置是在程序运行时动态分配的,与上面的定长串相较,更灵活有效,不怕存不下而舍弃某些字符。代价就是在程序运行中不断生成新串和销毁旧串。堆串在插入,删除,赋值三个操作上与定长串不大相同,其余基本一致,不多描述。
02-01-堆串插入函数
无需考虑原串空间,直接申请能够装下原串和新串的空间,然后挪就行了。
实现如下:
// 堆串插入 在串s中下标为pos的位置前插入串t
void StrInsert(string* s, int pos, string* t) {
if (pos<0 || pos>s->len || s->len == 0) return; // 老规矩,先看插入位置是否合法
char* new_s = (char*)malloc(s->len + t->len); // 申请大空间
if (new_s) { // 申请成功就可以进行后续
for (int i = 0; i < pos; i++) new_s[i] = s->S[i]; // 跟前面一样,新串分成三段,先将A段存进去
for (int i = 0; i < t->len; i++) new_s[i + pos] = t->S[i]; // 存B段,也就是要插入的串
for (int i = pos; i < s->len; i++) new_s[i + t->len] = s->S[i]; // 存剩余的C段
s->len += t->len;
free(s->S); // 释放原串
s->S = new_s; // 指向新串
}
}
02-02-堆串删除函数
删除与插入可以看作相反的过程,只需要将原串从所要删除的串处分为前中后,ABC三段,然后将C从B的起点开始存入即可。
实现如下:
// 删除函数 在s串中,删除从pos起的len个字符
void Strdelet(string* s, int pos, int len) {
if (pos<0 || pos>(s->len - len) || s->len == 0) return; // 参数不合法 需要注意的是删除的长度不能超过从删除位置开始的元素总个数,否则就会下标越界
char* new_s = (char*)malloc(s->len - len);
if (new_s) {
for (int i = 0; i < pos; i++) new_s[i] = s->S[i]; // 挪A段
for (int i = pos + len; i < s->len; i++) new_s[i - len] = s->S[i]; // 挪C段
s->len -= len;
free(s->S);
s->S = new_s;
}
}
02-03-堆串赋值函数
实际上赋值都是遍历一遍串值存储空间,不过是值存储的地方不同罢了。
// 赋值函数 将m的串赋给s
void StrAssign(string* s, char* m) {
int len = 0,i=0;
if (s->S != NULL) free(s->S); // 俗话说,先破后立
while (m[i] != '\0') ++i;
len = i;
if (len) {
s->S = (char*)malloc(len); // 这就叫革故鼎新
if (s->S) {
for (int i = 0; i < len; ++i) s->S[i] = m[i];
}
else return;
}
else s->S = NULL;
s->len = len;
}
定长串赋值思路一样,不过需要考虑两个串的长度关系。
03.块链串
由于串也是一种线性表,因而也可以采用链式存储。因为串是一个特殊的线性表(表中每个元素都是字符)。在实现时,可以在每个结点存储一个字符,也可以存放多个字符。其中每个结点都称为块,整个链表称为块链结构。为了便于操作,设置头尾两个指针。
其定义如下:
const int Block_Size = 4; // 每个结点存放4个字符
typedef struct Block {
char s[Block_Size];
struct Block* next;
};
typedef struct {
Block* head;
Block* tail;
int len;
};
拓展:
链表中的结点分成两个域data(数据域)与link(结点域)。以此链表为例,其中结点大小是指data域中存放字符的个数,链域大小是指link域占用字符的个数。如果一个结点大小为4,链域大小为2,根据存储密度公式,可知该字符串的存储密度为2/3.
存储密度,在计算机中是指结点数据本身所占的存储量和整个结点结构所占的存储量之比,计算公式:存储密度 = (结点数据本身所占的存储量)/(结点结构所占的存储总量)。这里的结构一般指的是数据结构,主要通过计算机中数据的存储结构来影响存储密度。
1、结构数据本身所占存储量 = 数据域所占存储量
2、结点结构所占的存储总量 = (数据域+结点域)所占存储量
————————————————
版权声明:本文为CSDN博主「MOSkami」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/h2809871142/article/details/88653565
可见,串的存储密度越小,运算处理就越方便,从少一个尾指针和多一个尾指针就可以看出来,不过相应的,牺牲了空间。
注意:
此块链串的Block_Sized大于1时,每个结点可以存放多个字符,当最后一个结点未存储满时,可以用一些特殊符号(如#)补齐空位。虽然存储密度相对于结点大小为1的存储方法来说,存储密度较高,但在进行插入,删除等操作时比较复杂,需要考虑结点的分拆与合并,或许又要多设置一些结点变量辅助操作,存储密度又会减小。但一切以实际为准。
本篇文章就到此结束,(*★,°*:.☆( ̄▽ ̄)/$:*.°★* 。)希望能对各位有所帮助,o(* ̄▽ ̄*)ブ