一、存储结构
字符串在计算机一般有三种表示方式:
- 定长顺序存储:将串定义为字符数组,串的存储空间在编译时确定,其大小不能改变。
- 堆分配存储:仍用一组地址连续的存储单元依次存储串中的字符序列,但串的存储空间是在程序运行时动态分配的,其使用的是程序的堆内存空间。
- 块链存储:使用链式存储结构。
Ⅰ.定长顺序存储
#define MAX_STRLEN 255
char str[MAX_STRLEN+1];
- 使用数组形式存储字符序列,数组以
'\0'
作为串结束标志,故声明数组长度时需用MAX_STRLEN+1
。 - 字符串长度可以是小于
MAX_STRLEN
的任何值,若超出长度限制,多余部分会被截断。
Ⅱ.堆分配存储
class HString{
int lenght;
char *ch;
};
- 需使用
new
与delete
动态分配与回收存储空间,由new
与delete
动态分配与回收的存储空间称为堆。 - 堆分配存储结构的串兼具顺序存储的特点,同时对串长没有限制,更显灵活。
Ⅲ.块链存储
#define CHUNKSIZE 80
struct Chunk{ //块结构
char ch[CHUNKSIZE];
Chunk *next;
};
class LString{
Chunk *head,*tail; //头尾指针
int curlen; //当前长度
};
二、匹配算法
以下算法实现具基于顺序存储。
Ⅰ.蛮力算法
- 从主串指定位置开始,将主串与模式串的第一个字符比较。
- 若相等,继续逐个比较后续字符。
- 若不等,从主串的下一个字符起重新与模式串第一个字符比较。
#include<string.h>
int match(char *P,char *T){
size_t n = strlen(T), i=0; //主串长度、指定起始比对位置
size_t m = strlen(P), j=0; //模式串长度,指定起始比对位置
while(i<n && j<m){
if(T[i]==P[j]) //若匹配
{ i++; j++; } //继续比对下一字符
else
{ i-=j-1; j=0 } //主串回到上次始配位置的下一位置,模式串回到第一个字符
}
}
string.h
头文件中的strlen()
函数可直接传入字符数值形式的串的指针,得到字符串的实际长度。
- 在最好情况下,除比较成功的位置外,其余位置只需比较一次(模式第一个字符),其时间复杂度为 o ( m + n ) 。 o(m+n)。 o(m+n)。
- 在最坏情况下,需要迭代n-m+1轮,且每轮需比较m次,此时时间复杂度为 o ( n ∗ m ) o(n*m) o(n∗m)。
- 实际上,当字符表比较大时,此类算法的最好(或接近最好)情况出现的概率并不很低。
Ⅱ.KMP算法
蛮力算法的低效来自于大量的局部匹配:每一轮的m次比对中,仅最后一次可能适配,而一旦发现失配,蛮力算法会使两个指针完全回退。而KMP算法的思想是:利用以往成功的比对所提供的信息,不仅可以避免文本串字符指针的回退,还可使模式串尽可能最大跨度地右移。
KMP算法的特点
当出现失配时( T [ i ] ≠ P [ j ] T[i]≠P[j] T[i]=P[j],其中P为模式串,T为文本串):
- 不需回溯指针 i i i
- 利用已经得到的“部分匹配”的结果
- 将模式上的指针尽可能短的回退一段距离后,继续进行比较
-
当出现失配时,经过前面的比对,已经确定的匹配范围为: P [ 0 , j ] = T [ i − j , i ] P[0,j] = T[i-j,i] P[0,j]=T[i−j,i];
-
若要使模式串的索引适当右移后,能够与T的某一个真后缀子串匹配,则需满足: P [ 0 , t ) = T [ i − t , i ) = P [ j − t , j ) P[0,t)=T[i-t,i)=P[j-t,j) P[0,t)=T[i−t,i)=P[j−t,j);
-
亦即,在在已匹配子模式串 P [ 0 , j ] P[0,j] P[0,j]中长度为 t t t的真前缀,应与其长度为 t t t的真后缀完全匹配,故 t t t必来自集合: N = { 0 ≤ t ≤ p ∣ P [ 0 , t ] = P [ j − t , j ) } N=\{0≤t≤p \ |\ P[0,t]=P[\ j-t,j)\} N={0≤t≤p ∣ P[0,t]=P[ j−t,j)}。一般地,该集合可能包含多个这样的 t t t,但需注意的是, t t t值仅取决于模式串P与比对时的首个失配位置 P [ j ] P[j] P[j],而与文本串无关。
-
此时,若下一轮的比对将从 T [ i ] T[i] T[i]与 P [ t ] P[t] P[t]的比对开始,这等效于将 P P P右移 j − t j-t j−t个单元,位移量与 t t t成反比。因此,为使 T T T的上一轮失配位置(索引 i i i)绝不倒退,同时又最大程度利用已匹配结果,应在集合 N N N中挑选最大的 t t t。也就是说,应选择其中移动距离最短者。
-
于是若令 n e x t [ j ] = m a x ( N ) next[j]=max(N) next[j]=max(N),则一旦发现 T [ i ] ≠ P [ j ] T[i]≠P[j] T[i]=P[j]失配,即可转而将 P [ n e x t [ j ] ] P[next[j]] P[next[j]]与 T [ i ] T[i] T[i]对齐,并从改位置进行下一轮的匹配。
-
由于集合 N N N( t t t的值)仅取决于模式串 P P P以及失配位置 j j j,而与文本串无关。于是对于给定的任意模式串中的元素,都有着其特有的、确定的 n e x t [ j ] next[j] next[j],不妨依据模式串预先计算出所有位置 j j j对应的 n e x t [ j ] next[j] next[j]值,以便后续查询。至此,可将已匹配部分的"记忆力"转换为具有普适性的"预知力"。
此时将串匹配算法的问题转换为求模式串的 n e x t next next数组,若模式串的 n e x t next next求得,则可进行高效的串匹配。
next[j]的求取
-
n e x t [ 1 ] = 0 , n e x t [ 2 ] = 1 next[1]=0,next[2]=1 next[1]=0,next[2]=1
若在模式串第一个位置处失配,则将文本串的指针右移,直到出现与模式串第一个字符相匹配的字符,用 n e x t = 0 next=0 next=0来标识这样的情况;而若在第二个字符失配,则将模式串指针退回至首个字符。
-
n e x t [ j + 1 ] next[j+1] next[j+1]
即若已知 n e x t [ 0 , j ] = t next[0,j]=t next[0,j]=t,要求取 n e x t [ j + 1 ] next[j+1] next[j+1]。
① n e x t [ j ] = t next[j]=t next[j]=t,则意味着在 P [ 0 , j ] P[0,j] P[0,j]中,自匹配的真前缀与真后缀的最大长度为 t t t,此时 P [ j ] = P [ t ] P[j]=P[t] P[j]=P[t],而若 P [ j + 1 ] = P [ t + 1 ] P[j+1]=P[t+1] P[j+1]=P[t+1],则 n e x t [ j + 1 ] = t + 1 next[j+1]=t+1 next[j+1]=t+1,即自匹配的最大长度加一。② 若 P [ j + 1 ] ≠ P [ t + 1 ] P[j+1]≠P[t+1] P[j+1]=P[t+1],则 n e x t [ j + 1 ] next[j+1] next[j+1]的候选者应该是: n e x t [ n e x t [ j ] ] + 1 , n e x t [ n e x t [ n e x t [ j ] ] ] + 1 , . . . next[next[j]]+1,next[next[next[j]]]+1,... next[next[j]]+1,next[next[next[j]]]+1,...,只需反复用 n e x t [ t ] next[t] next[t]替换 t t t( t = n e x t [ t ] t=next[t] t=next[t]),直到 P [ j ] = P [ t ] P[j]=P[t] P[j]=P[t],即可按优先次序遍历以上候选者,从而选出最大的 n e x t next next值。
以上第②点的原理:
由于在探寻 n e x t [ ] next[\ ] next[ ]数组的过程中,模式串本身即作文本串也作模式串,当在求取 n e x t next next若出现失配的情况( P [ j ] ≠ ] P [ t ] P[j]≠]P[t] P[j]=]P[t])时,根据KMP算法的原理,模式串 P [ 0 , t ] P[0,t] P[0,t]的指针 t t t也必然要根据已求得的 n e x t next next进行回溯,于是有 t = n e x t [ t ] t=next[t] t=next[t]。
求取给定串的 next数组算法:
void GetKmpNext(char *pString,int *Next){
//求取模式串的pString的next函数值并存入数组Next
//其中Next数组的第0号元素放置串长度
int j,t;
Next[0] = len(pString); //len()函数需自行实现
j=2;
Next[1]=0;
t=Next[1];
while(t<=Next[0]){
if(t==0||pString[j]==pString[t-1]){
Next[j]=t+1;
j++;
t=Next[j-1];
}
else{
t=Next[t];
}
}
}
next数组第一个元素的索引
index
及其存放的值val
不能一样,当q=index
时,表示与第一个元素匹配,自匹配的最大真前后缀为1;当q=val
时,表示无相同前后缀(完全失配),主串指针后移。
且一般val=index-1
,以便在完全失配时,通过next[p]=q+1
的方式,将该位的next值设为第一个元素的索引,即完全失配时跳到第一个元素。
一般next[]
数组有两种形式:
① 第一个next值从索引0开始,此时next[0]=-1
;
② 第一个next值从索引1开始,此时next[1]=0
,而next[0]
存放无意义的值(如模式串的长度)。