文章目录
一、串的抽象类型定义
1. 字符串的基本操作
- 基本字符串的操作函数都在 字符函数、字符串函数及内存函数 中,本文只做简单介绍。
二、串的存储结构
- 与线性表类似串也有两种基本存储结构:顺序存储与链式存储。
- 但考虑到存储效率和算法的方便性,串多采用顺序存储结构。
1. 串的顺序存储结构
- 类似于线性表的顺序存储结构,用一组地址连续的存储单元存储串值的字符序列。
- 按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区,则可用固定长度的数组来描述。
#define MAX 255//字符串的最大长度
typedef struct
{
char ch [MAX + 1];//存储串字符串的一维数组,还应该包含一个最后的\0
int length;//串的当前长度
}SString;
- 为了便于理解,后面算法描述当中所用到的顺序存储的串都是从下标为1的数组位置开始存储的,下标为 0 的位置闲置不用。
2. 串的链式存储结构
优点:操作方便。
缺点:存储密度较低。
- 每个结点用来存储字符的空间需要1个字节,然而每个结点的指针域却要占4个字节,每个结点就需要占用5个字节,结点的存储密度就很低了。
- 为了克服这个缺点,可以将多个字符放在一个结点,
- 这样子每个结点由4个字节的空间可以拿来存储数据,另外4个字节拿来存储地址,这样每个结点的存储密度就变成了4/8 = 50%了
-
通常把这样的一个结点称为块;
- 通常这样一个块可以放更多的字符,比如放 50 个字符,这样存储密度就 变成了 92%,但是操作起来仍然是很方便的。
-
通常把这种结构称为:块链结构
块链结构
//串的链式存储结构
#define CHUANSIZE 80 //块的大小可由用户定义
typedef strcut Chunk
{
char ch[CHUANSIZE];
struct Chunk* next;
}Chunk;
typedef struct
{
Chunk* head;//串的头指针
Chunk* tail;//串的尾指针
int length;//串的当前长度
}LString;//字符串的块链结构
三、串的模式匹配算法
算法目的
- 确定主串中所含子串(模式串)第一次出现的位置(定位)。
- 如果主串中包含子串,则返回子串的第一个字符在主串中的位置,反之返回 0。
算法应用
- 搜索引擎、拼写检查、语言翻译、数据压缩。
算法种类
- BF 算法(Brute-force,又称古典的、经典的、朴素的、穷举的)。
- KMP 算法(特点:速度快)。
1. BF 算法
-
最简单直观的模式匹配算法是 BF 算法。
-
模式匹配算法不一定是从主串的第一个位置开始,可以指定主串中查找的起始位置 pos。
-
让子串在主串中的位置一位一位的往后移,然后和主串中的内容进行比较,直到在主串中找到子串的内容。
- 算法的思路是从 S 的每一个字符开始依次与 T 的字符进行匹配。
举个栗子
- 设目标串 S = “aaaaab”,模式串(子串)T = “aaab”。
- S 的长度为 n(n = 6),T的长度为 m(m = 4)。
BF算法的匹配过程如下
- 先从第一个字符开始比较,前面说过,为了方便,将下标为1的位置作为字符存放的第一个位置,下标 0 的位置闲置。
- 两个串的第一个字符都一样则 i++ 和 j++,用 S.ch[i] 和 T.ch[j] 来进行比较。
- 如果比较到不相等字符的位置的时候,接下来要让 i 退到第二个字符的位置,然后从主串的第二个字符的位置开始依次和子串的每个字符开始一一比较。
- i 退到第二个字符的位置,再和子串一一比较
- 等到两个串出现不匹配的字符的时候,则让 i 退到第3个字符的位置,继续重复以上步骤。
- i 开始的位置 = 结束的位置(i)- 移动的距离(j - 1)
- 直到任何一个串走到尽头,后面没字符可以拿来比较的时候,如果两个串都没有出现不匹配的字符的话,则说明这两个串是主串和子串的关系。
- 此时 i 和 j 的位置分别是7和5,此时直接用 i (7)减掉匹配的串的长度 aaab(4),就可以得出子串在主串中的位置3了。
算法步骤
Indes(S,T,pos)
- 将主串的第 pos 个字符和模式串的第一个字符进行比较。
- 若相等,继续逐个比较后续字符。
- 若不等,则从主串的下一个字符起,重新与模式串的第一个字符进行比较。
- 直到主串的一个连续子串字符序列与模式串相等。返回值为 S 中与 T 匹配的子序列的第一个字符的序号,即匹配成功。
- 反之匹配失败,返回值为 0 。
算法描述
//返回模式T在主串S中第pos个字符开始第一次出现的位置,若不存在则返回0。
//其中T非空,1 <= pos <= S.length
int Index_BF(SString S,SString T,iny pos)
{
i = pos;j = 1;//初始化
//两个串均未达到串尾
while(i <= S.length && j <= T.length)
{
//主子串对应位置字符如果相等则比较后续字符
if(S.ch[i] == T.ch[j])
{
i++;
j++;
}
else//主串、子串指针回溯重新开始下一次匹配
{
i = i -j + 2;
j = 1;
}
}
if(j > T.length)//匹配成功
{
return i - T.length;
}
else//匹配失败
{
return 0;
}
}
BF算法的时间复杂度
最好情况
- 每趟不成功的匹配都发生在模式串的第一个字符与主串中相应字符的比较。例如:
- S = “aaaaaba”
- T = “ba”
最坏情况
- 每趟不成功的匹配都发生在模式串的最后一个字符与主串中相应字符的比较。
-
如果是平均的情况下时间复杂度为O(n/2 * m),又因为在算时间复杂度的时候系数可以去掉,所以平均时间复杂度还是O(n * m)。
-
这里让比较指针 回溯 就是造成整个算法效率比较低下的原因。
2. KMP 算法
BF算法效率底下的原因
- 让主串和模式串挨个字符进行比较
- 如果遇到了不相等的字符则让模式串后移一位,并且让指针重新指向模式串的第一个字符,然后继续继续进行比较。
KMP算法设计思想
- 利用已经部分匹配的结果而加快模式串的滑动速度。
- 且主串 S 的指针 i 不必回溯!可提速到 O(n+m)!
KMP 算法的做法就是仅仅后移模式串就能让两个串的每一个字符进行比较,从而找出子串。
【例1】
- 回到一开始发现第一个出现字符不匹配的位置,可以发现,不匹配的字符(箭头)左边的字符主串和模式串是完全匹配的。
- 模式串左右两端都有一个相等 AB 两个子串,这两个相等的子串称为模式串的公共前后缀。
- 核心:接下来直接移动模式串,将公共前后缀里的前缀直接移动到了原来的后缀处,
- 这样移动了之后就会让比较指针左边的主串和模式串中的内容一致,因为公共前后缀的值是相匹配的,移动之前左边主串和模式串的内容是匹配的,移动之后当然也是匹配的。
只要记录下出现不匹配的字符的位置,然后在字符指针前面的字符内容中找出模式串的公共前后缀,最后再直接将前缀移动到后缀,这样就可以继续往下比较了,省时省力。
- 注意:如果模式串中出现了多对公共前后缀,要取最长的那对
- 后缀的位置一定是与不匹配的位置相邻的。
- 当走到下一个不匹配的位置时,继续重复以上步骤,寻找最长公共前后缀,然后将前缀移动到后缀的位置。
- 与后缀对称相等的字符(或字符串)就称为前缀
- 这个时候发现,模式串已经超出了主串的范围,已经可以判定匹配失败,主串中不含有和模式串相同的子串。
【例2】
- 最长公共前后缀应该要小于标记指针左边的子串,如果前后缀取下图这样子的那就没有意义了。
- 所以模式串的最长公共前后缀应该是这样的。
- 让前缀移动到后缀的位置。
- 移动完了之后继续比较,找到不相等的字符的话,就继续找 最长公共前后缀,然后移动。
- 移动好了之后再继续比较模式串,比较指针一直移动到最后都没有找到不匹配的字符,这时候就可以判断主串中有包含模式串的子串。
挖掘模式串
- 使用 KMP 算法的过程中可以发现,在移动模式串的时候压根不需要用到主串,把主串扔掉照样能将前缀移动到后缀。
- 这样将模式串的内容挖出来之后就可以和任何的主串进行比较了,前面说过,模式串从数组下标为1的位置开始存储比较方便观看。
- 假设模式串和子串第一位就出现了不匹配,因为标记指针左边的子串的内容直接等于公共前后缀,前面说过,指针左侧的长度不能等于公共前后缀的长度,所以此处模式串指针只能移动1位。
- 让模式串中1号位的字符与主串中二号位的字符进行比较,如果还不相等,则让模式串中的第1位字符和主串的第三位字符比较。公共前后缀长度为0的时候,模式串只能移动1位。
- 在比较指针移动到主串的4号位的时候发现,公共前后缀长度终于不为0了,操作还是一样,前缀——>后缀。
- 然后让模式串的 2 号位与主串当前位置(4号位)的字符进行比较
- 找主串的 5 号位对应的模式串左边的最长公共前后缀,然后移动模式串,让模式串当前的 3 号位与主串当前位置(5号位)的字符进行比较。
- 然后看对应主串 6 号位的情况,操作依然不变,找公共前后缀,移动位置,模式串 4 号位与主串当前位(6号位)比较。
总结
- 如果公共前后缀长度为 n 。
- 我们就得到将模式串的 n + 1号位与主串当前位开始比较。
- 每次开始比较的编号就是最大公共前后缀长度+1
- 比如:此时比较指针到了模式串的 7 号位置,左边的最大公共前后缀长度是1,移动之后就是将模式串的2号位置的字符与主串的当前位置进行比较。
噼里啪啦一顿操作之后
- 我们将第一句话的内容给他标记为 0,当看到这个 0 的时候就按照第一句话描述的内容(1号位与主串下一位比较)来处理,if(0){则按照第一句话的内容来做}
- 将后面每句话对应的数字都拿出来,作为每句话的内容所要执行的代号。
- 然后再结合上面的数组下标,将他们放到一个数组中,这样一来根据数据中所提供的信息,任何一个位置发生了不匹配,自动就知道下一步该怎么做了。
- 比如:模式串的0号位置(对应数组的1号位)不匹配,自然知道该干啥。这样一个数组就称为 next 数组。