算法与数据结构【算法】——四、字符串匹配:基础字符串、朴素匹配与KMP
一、串的抽象数据类型定义
(1)数据对象:
D={ ai |ai∈CharacterSet, i=1,2,…,n, n≥0 }
(2)数据关系:
R1={ < ai-1, ai > | ai-1, ai ∈D,i=2,…,n }
(3)基本操作:
操作 | 操作 | 操作 |
---|---|---|
StrAssign (&T, chars) 创建并赋值 | StrCopy (&T, S) 串复制 | DestroyString (&S) 串销毁 |
StrEmpty (S) 判断空串 | StrCompare (S, T) 串比较 | StrLength (S) 求串长 |
Concat (&T, S1, S2) 串连接 | SubString (&Sub, S, pos, len) 串截取,pos起len个,返回到Sub | StrDelete (&S, pos, len) 串删除,pos起len个 |
Replace (&S, T, V) 串替换(不重叠) | StrInsert (&S, pos, T) 串插入,pos位之前 | Index (S, T, pos) 在pos位后查找,返回在主串位置 |
ClearString (&S) 串清空 |
- 最小操作子集
- 串赋值StrAssign、串复制Strcopy、 串比较StrCompare、求串长StrLength、 串联接Concat、求子串SubString
- 以上操作能构成其他基本操作。( 除串清除ClearString和串销毁DestroyString )
二、串与线性表的区别
- 串的数据对象约束为字符集
- 串的基本操作和线性表有很大差别。
- 在线性表的基本操作中,大多以“单个元素”作为操作对象;
- 在串的基本操作中,通常以“串的整体”作为操作对象。
三、串的表示和实现
(1)串的定长顺序存储表示
#define MAXSTRLEN 255 // 在255以内定义最大串长
typedef unsigned char Sstring[MAXSTRLEN + 1]; // 0号单元存放串的长度
- 串的实际长度可在这个定义长度的范围内随意设定
- 超过定义长度的串值则被舍去,称之为“截断”
- 合并字符串:a. 情况一:未截断,S1+S2 b. 情况二:S1+部分S2 c. 情况三:部分S1
- 基本操作: “字符序列的复制”
(2)串的堆分配存储表示
typedef struct {
char *ch; // 若是非空串,则按串长分配存储区,否则ch为NULL
int length; // 串长度
} HString;
- C的串类型属于该法存储,以一个空字符为结束符,串长是一个隐含值。
- 系统利用函数malloc( )和free( )进行串值空间的动态管理,为每一个新产生的串分配一个存储区,即"堆",称串值共享的存储空间
- 实现的算法为:先为新生成的串分配一个存储空间,然后进行串值的复制。
- 合并字符串:释放旧空间,申请新空间,直接复制
- 求子串:
Sub.ch = (char )malloc(lensizeof(char));
Sub.ch[0…len-1] = S[pos-1…pos+len-2];
Sub.length = len;
(3)串的块链存储表示
- 可用链表来存储串值,通常一个结点中存放的不是一个字符,而是一个子串
- 原因:(由于串的数据元素是一个字符,它只有 8 位二进制数)
- 代码实现:
#define CHUNKSIZE 80 // 可由用户定义的块大小
typedef struct Chunk{ // 结点结构
char ch[CUNKSIZE];
struct Chunk *next;
}Chunk;
typedef struct{ // 串的链表结构
Chunk *head, *tail; // 串的头和尾指针
int curlen; //串的当前长度
}Lstring;
四、串的模式匹配算法——单模式串匹配
串匹配函数:INDEX (S, T, pos) ,S 主串,T 模式串
单模式串匹配:一个串跟一个串进行匹配 —— BF 算法 和 RK 算法 —— BM 算法 和 KMP 算法
多模式串匹配算法:一个串中同时查找多个串 —— Tire 树 和 AC自动机
(1)简单算法
- BF算法, Brute Force 的缩写,中文叫作暴力匹配算法,也叫朴素匹配算法
- 在主串S中,检查起始位置分别是 0、1…n-m 且长度为 m 的 n-m+1 个子串,看有没有跟模式串匹配的
- 时间复杂度:O(n*m)
- 常用算法——KISS(Keep it Simple and Stupid)设计原则
int Index(SString S, SString T, int pos) { // 返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数值为0。
i = pos; j = 1;
while (i <= S[0] && j <= T[0]) {
if (S[i] == T[j]) { ++i; ++j; } // 继续比较后继字符
else { i = i-j+2; j = 1; } // 指针后退重新开始匹配
}
if (j > T[0]) return i-T[0]; else return 0;
} // Index
- RK算法——优化
- Step1:通过哈希算法对主串中的 n-m+1 个子串分别求哈希值
- Step2:逐个与模式串的哈希值比较大小,若相等则匹配
- 将字符串匹配改为数组匹配,效率提升,但是算法整体的效率并没有提高
- 考虑26进制哈希,则相邻哈希值有相关性,则提升算法效率——O(n)
- 哈希冲突?
(2)首尾匹配算法
- 先比较模式串的第一个字符,再比较模式串的最后一个字符,最后比较模式串中从第二个到第n-1个字符。
(3)KMP算法 O(m+n)
- BM 算法 到 KMP算法 (此处之后再细看)
- 原理
- 当 S[i] <> T[j] 时,已经得到的结果:S[i-j+1…i-1] == T[1…j-1] (前面相等,新的匹配不等)
- 若已知 T[1…k-1] == T[j-k+1…j-1] (已知匹配传的字串有重复, 回缩),则有 S[i-k+1…i-1] == T[1…k-1]
- 1、串匹配
int IndexKMP(SStringS, Sstring T, int pos){
i=pos; j=1;
if (j=0 || S[i]==T[i]) {++i; ++j;} //继续比较后继字符
else j=next[j]; //模式串向右移动
if (j>T[0]) return i-T[0]; /*匹配成功*/ else return 0;
}
- 2、自匹配——求next
- 主串和模式串是同一个串 的 自匹配
Void get_next(String &T,int &next[]){ //求模式串T的next函数值并存入数组next
i=1; next[1]=0;j=0;
while (i<T[0])
if (j==0 || T[i]==T[j]) { ++i; ++j; next[i]=j; } else j=next[j];
}