字符串匹配算法
1. 朴素的匹配算法(暴力匹配)
寻找子串p在主串s中第pos个字符后的位置。
分别利用计数指针i和j指示主串s和子串p中当前待比较的字符。算法的基本思想是:从主串s的第pos个字符起和模式的第一个字符比较,如果相等,继续逐个比较后续字符;否则从主串的下一个字符起,重新和模式的第一个字符比较。算法的时间复杂度为O(n*m)。
代码:
/* * 输入:zhangleilei is short of lei. * lei * 输出:5 8 24 * * 输入:aaaaaa * aaa * 输出:0 3 */ #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> #include <malloc.h> #define MAXLENGTH 100 int *violentSearch(char *s, char *p, int pos); int main() { char s[MAXLENGTH], p[MAXLENGTH]; int i; //result[0]用于存放匹配子串的个数,数组的其它元素用于存放所有匹配子串第一个字符的下标。 int *result; gets(s); gets(p); result = violentSearch(s, p, 0); if (result[0] == 0) printf("No matching string.\n"); else { for (i = 1; i <= result[0]; i++) { printf("%-3d", result[i]); } } return 0; } /* * 返回子串t在主串s中第pos个字符之后的位置。 * 位置存放于数组result中,result[0]表示子串的个数。 */ int *violentSearch(char *s, char *p, int pos) { int i = pos, j; int len_s = strlen(s), len_p = strlen(p); int *result, k = 1; //匹配字符串的个数最多为(len_s - pos) / len_p,再者,result[0]用于存放总数量。 //所以为result数组分配的空间大小为(len_s - pos) / len_p + 1。 result = (int*)malloc(sizeof(int)*((len_s - pos) / len_p + 1)); while (i < len_s) { j = 0; while (i < len_s && j < len_p) { if (s[i] == p[j]) { i++; j++; //继续比较后继字符 } else { i = i - j + 1; j = 0; //指针后退,重新开始匹配 } } if (j == len_p) { result[k++] = i - j; } } result[0] = k - 1; return result; }
2. KMP算法
KMP算法的核心思想是利用已经得到的部分匹配信息来进行后面的匹配过程。
最大特点:指示主串的指针不需回溯,整个匹配过程中,对主串仅需从头至尾扫描一遍。
如果文本串的长度为n,模式串的长度为m,那么匹配过程的时间复杂度为O(n),算上计算next的O(m)时间,KMP的整体时间复杂度为O(m + n)。
更好地理解KMP算法:字符串匹配的KMP算法
更好地理解KMP代码:从头到尾彻底理解KMP
几个关键点:
next数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next[j]=k,代表j之前的字符串中有最大长度为k的相同前缀后缀。此也意味着在某个字符失配时,该字符对应的next值会告诉你下一步匹配中,模式串应该跳到哪个位置(跳到next[j]的位置)。如果next[j]等于0或-1,则跳到模式串的开头字符,若next[j]=k且k>0,代表下次匹配跳到j之前的某个字符,而不是跳到开头,且具体跳过了k个字符。
KMP的next数组相当于告诉我们:当模式串中的某个字符跟文本串中的某个字符匹配失配时,模式串下一步应该跳到哪个位置。如模式串中在j处的字符跟文本串在i处的字符匹配失配时,下一步用next[j]处的字符继续跟文本串i处的字符匹配,相当于模式串向右移动j-next[j]位。
next数组的代码递归实现算法:已知next[0,...,j],如何求出next[j+1]呢?
对于p的前j+1个序列字符:(k=next[j])
l 若p[k]==p[j],则next[j+1]=next[j]+1=k+1;
下标 |
0 |
1 |
2 |
3 |
4 |
5 |
6 |
P[] |
A |
B |
C |
A |
B |
C |
E |
Next[] |
|
|
|
|
|
2 |
3 |
|
|
|
k |
|
|
j |
j+1 |
l 若p[k]≠p[j],如果此时p[next[k]]==p[j],则next[j+1]=next[k]+1,否则继续递归重复此过程。相当于在字符p[j+1]之前不存在长度为k+1的前缀"p0p1,…,pk-1pk"跟后缀“pj-kpj-k+1,…,pj-1pj"相等,那么是否可能存在另一个值t+1<k+1,使得长度更小的前缀“p0p1,…,pt-1pt”等于长度更小的后缀“pj-tpj-t+1,…,pj-1pj”呢?如果存在,那么这个t+1便是next[j+1]的值,此相当于利用next数组进行P串前缀跟P串后缀的匹配。
若能在前缀“ p0 pk-1 pk ” 中不断的递归k = next [k],找到一个k’,使p[k’] = p[j],且满足p0 pk'-1 pk' = pj-k' pj-1 pj,则最大相同的前缀后缀长度为k' + 1,从而next [j + 1] = k’ + 1。否则如果前缀中找不到这样的k’,则代表没有相同的前缀后缀,next [j + 1] = 0。(递归结束条件:k = -1(next[0]的值),此时next[j+1] = k + 1 = 0)
A |
B |
C |
A |
B |
C |
D |
A |
B |
C |
A |
B |
A |
C |
|
|
|
|
|
k |
|
|
|
|
|
|
j |
j+1 |
因为p[j] != p[k]
递归求k = next[k]
A |
B |
C |
A |
B |
C |
D |
A |
B |
C |
A |
B |
A |
C |
|
|
k |
|
|
|
|
|
|
|
|
|
j |
j+1 |
因为p[j] != p[k]
继续递归求k = next[k]
A |
B |
C |
A |
B |
C |
D |
A |
B |
C |
A |
B |
A |
C |
k |
|
|
|
|
|
|
|
|
|
|
|
j |
j+1 |
此时p[j] == p[k]
next[j+1] = k+1 = 1
求next数组的代码:
/* * 先已知next[0] = -1,再依次递归为next[1~...]的值。 */ void GetNext(char *p, int *next) { int plen = strlen(p); int j = 0, k = -1; next[0] = -1; //j始终指向已求出next值的最后一个字符。 while (j < plen-1) { //k指向前缀的最后一个字符,j指向后缀的最后一个字符。 //递归k = next[k],直到p[j] == p[k],或者k = next[0] = -1无法继续递归为止。 if (k == -1 || p[j] == p[k]) { k++; j++; next[j] = k; } else k = next[k]; } }
next数组的优化:
A |
B |
A |
C |
A |
B |
A |
B |
D |
A |
B |
A |
B |
|
|
|
|
|
|
k |
|
j |
|
|
|
|
|
B与C失配,按照先前的算法,next[3] = 1,模式串向右移动3-next[3] = 2位。
A |
B |
A |
C |
A |
B |
A |
B |
D |
|
|
A |
B |
A |
B |
|
|
|
第一次的时候绿色的B已经和C比较过,第二次蓝色的B又和C比较了一次。此为冗余比较。造成冗余比较的原因是p[j] == p[next[j]]。当p[j] != s[i] 时,下次匹配必然是p[ next [j]] 跟s[i]匹配,如果p[j] = p[ next[j] ],必然导致后一步匹配失败,所以不能允许p[j] = p[ next[j] ]。
优化后的代码:
/* * 先已知next[0] = -1,再依次递归为next[1~...]的值。 */ void GetNext(char *p, int *next) { int plen = strlen(p); int j = 0, k = -1; next[0] = -1; //j始终指向已求出next值的最后一个字符。 while (j < plen-1) { //k指向前缀的最后一个字符,j指向后缀的最后一个字符。 //递归k = next[k],直到p[j] == p[k],或者k = next[0] = -1无法继续递归为止。 if (k == -1 || p[j] == p[k]) { k++; j++; if (p[j] != p[k]) next[j] = k; //因为不能出现p[j] = p[ next[j ]],所以当出现时需要继续递归 else next[j] = next[k]; } else k = next[k]; } }
测试代码:(加上上述求next数组的代码)
/* * 寻找主串s中是否包含子串p,如果包含,输出所有子串首字符的下标。 * 输入:Don't trouble trouble until trouble troubles you. * trouble * 输出:6 14 28 36 * 输入:aaaaaa * aaa * 输出:0 3 */ #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <string.h> #include <malloc.h> #define MAXLENGTH 100 void GetNext(char *p, int *next); int *KMPSearch(char *s, char *p, int pos); int main() { char s[MAXLENGTH], p[MAXLENGTH]; int *result; int i; gets(s); gets(p); result = KMPSearch(s, p, 0); if (result[0] == 0) printf("No matching string!\n"); else { for (i = 1; i <= result[0]; i++) printf("%-3d", result[i]); } return 0; } int *KMPSearch(char *s, char *p, int pos) { int i = pos; int j; int slen = strlen(s); int plen = strlen(p); int *result, k = 1; int *next; result = (int*)malloc(sizeof(int)*((slen - pos) / plen + 1)); next = (int*)malloc(sizeof(int)*plen); result[0] = 0; GetNext(p, next); while (i <= slen - plen) { j = 0; while (i < slen && j < plen) { //不要漏掉j == -1的情况。 //当主串中的某个字符正和模式串的第一个字符比较,且两个字符不匹配之后,j值变为-1。 //此时,指向主串和模式串的指针均要加1。 if (j == -1 || s[i] == p[j]) { i++; j++; } else { //next[j]表示当前字符j失配后,需要与主串失配字符比较的模式串字符。 j = next[j]; } } if (j == plen) { result[k++] = i - j; } } result[0] = k - 1; return result; }