举例来说,有一个字符串 Str1 = “BBC ABCDAB ABCDABCDABDE”,判断,里面是否包含另一个字符串 Str2 = “ABCDABD”?
1.首先,用Str1的第一个字符和Str2的第一个字符去比较,不符合,关键词向后移动一位
5.遇到Str1有一个字符与Str2对应的字符不符合。
6.这时候,想到的是继续遍历Str1的下一个字符,重复第1步。(其实是很不明智的,因为此时BCD已经比较过了,没有必要再做重复的工作,一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是”ABCDAB”已经比较过了。KMP 算法的想法是,设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。)
7.怎么做到把刚刚重复的步骤省略掉?可以对Str2计算出一张《部分匹配表》,这张表的产生在后面介绍
8.已知空格与D不匹配时,前面六个字符”ABCDAB”是匹配的。查表可知,最后一个匹配字符B对应的”部分匹配值”为2,因此按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值 (在代码中思路不必套这个公式)
因为 6 - 2 等于4,所以将搜索词向后移动 4 位。
9.因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(”AB”),对应的”部分匹配值”为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移 2 位。
10.因为空格与A不匹配,继续后移一位。
11.逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动 4 位。
12.逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动 7 位,这里就不再重复了。
- 接着比较字符串和搜索词的下一个字符,还是符合。
- 一直重复,直到Str1有一个字符与Str2的第一个字符符合为止
- 重复第一步,还是不符合,再后移
版本一(JAVA版本的kmp next数组并0开始)
package kmp;
/**
* 搜索思路分析:
* 情况一;
* 遇到不匹配的字符,主串的指针不必移动,模式串的指针移动到 对应模式串不匹配字符下标next数组中对应的数值
* 相当于后缀与前缀中间的字串已经匹配过了,不必再匹配
* 且next数组中记录的是前后缀最长公共长度。
*
* 所以字符串的第一个字符(即前缀的第一个字符)跳转到后缀的第一个字符位置
* [即前缀表达式跳转到后缀表达式的位置]
* 但是模式串并不重新再扫描前缀,因为前后缀字符串一样,而是从前缀的下一个字符开始进行匹配
*
* 情况二;
* 仅仅匹配了两个字符比如AB
* 则模式串的指针再次跳转,跳转到0 j = 0 , i = 10;
* 在for循环的条件下进行跳转匹配 j = 0 , i = 11 匹配 j +1
* 直到再次不匹配,j再跳转
*
* */
/**next数组不是从-1开始的,没有右移*/
public class KMP {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
//int[] next = kmpNext(str2);
//System.out.println("next = " + Arrays.toString(next));
int index = kmpSearch(str1, str2);
System.out.println("index=" + index); // 15
}
public static int[] kmpNext(String pattern){
int[] next = new int[pattern.length()];//next数组
int maxMatchLength = 0;//最大前后缀公共匹配长度
next[0] = 0;//一个字符的字符串匹配值为0
for(int i = 1 ; i < pattern.length() ; i++){
while (maxMatchLength > 0 && pattern.charAt(maxMatchLength) != pattern.charAt(i)){
maxMatchLength = next[maxMatchLength - 1];//这里的 - 1和搜索的 - 1意思不一样
//搜索的减一 是数组为了代码的方便
/**
* i指针指向的后缀字符串与maxMatch指向前缀字符串不匹配了
* maxMatch指针往后撤,直到后缀与前缀字符串相等,就停止
* 即为最长前后缀公共匹配长度
*
* */
}
if( pattern.charAt(maxMatchLength) == pattern.charAt(i)){
maxMatchLength++;
}
next[i] = maxMatchLength;
}
return next;
}
public static int kmpSearch(String text, String pattern){
int j = 0 ;
int[] next = kmpNext(pattern);
for(int i = 0 ; i < text.length() ; i++){
//模式串与字符串不匹配的时候就跳转,相当于数组直接跳到字符串最大后缀匹配第一个
//即数组前缀第一个字符移动到后缀的第一个字符的位置
//相当于 字符串长度 - 最长前后缀公共匹配长度 = 这么长的长度 字符串整体移动这么长
//因为在前缀和后缀之间的字符串已经匹配过了,再匹配也没有意思了
while ( j > 0 && pattern.charAt(j) != text.charAt(i)){
//跳转到后缀的第一个字符的位置
//分析:其实前缀和后缀的字符串是一样的,这一部分也不用比较了
//直接从最长公共前缀的下一位直接比较即可
//
j = next[j - 1];
}
if(pattern.charAt(j) == text.charAt(i)){
j++;
}
//完全匹配,输出下标位置
if(j == pattern.length()){
return i - j + 1;
}
}
return - 1;
}
}
韩顺平老师Java数据结构与算法课上讲的代码
package kmp;
import java.util.Arrays;
/**
* 搜索思路分析:
* 遇到不匹配的字符,主串的指针不必移动,模式串的指针移动到 对应模式串不匹配字符下标next数组中对应的数值
* 相当于后缀与前缀中间的字串已经匹配过了,不必再匹配
* 且next数组中记录的是前后缀最长公共长度。
*
* 所以字符串的第一个字符(即前缀的第一个字符)跳转到后缀的第一个字符位置
* 但是模式串并不重新再扫描前缀,因为前后缀字符串一样,而是从前缀的下一个字符开始进行匹配
* */
public class KMPAlgorithm {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int[] next = kmpNext(str2);
System.out.println("next = " + Arrays.toString(next));
int index = kmpSearch(str1, str2, next);
System.out.println("index=" + index); // 15了
}
//写出我们的kmp搜索算法
/**
*
* @param str1 源字符串
* @param str2 子串
* @param next 部分匹配表, 是子串对应的部分匹配表
* @return 如果是-1就是没有匹配到,否则返回第一个匹配的位置
*/
public static int kmpSearch(String str1 , String str2 ,int[] next){
for(int i = 0 ,j = 0 ;i < str1.length() ; i++){
while(j > 0 && str1.charAt(i) != str2.charAt(j)){
j = next[j - 1];
}
if(str1.charAt(i) == str2.charAt(j)){
j++;
}
if(j == str2.length()){
return i - j + 1;
}
}
return -1;
}
//dest是子串
public static int[] kmpNext(String dest){
int[] next = new int[dest.length()];
next[0] = 0;//如果字符串是长度为1 部分匹配值就是0
for(int i = 1 , j = 0 ; i < dest.length() ; i++){
while (j > 0 && dest.charAt(i) != dest.charAt(j)){
j = next[j - 1];//不匹配的前一位
}
if(dest.charAt(i) == dest.charAt(j)){
j++;
}
next[i] = j;
}
return next;
}
}
C语言版本(查找出主串所有匹配的位置 next数组由prefix数组右移一位得到)
#include<stdio.h>
#include<string.h>
#include <stdlib.h>
/*这个next数组是从-1开始的版本*/
//当两个字符串不适配的时候,
//回溯回退的位置始终是模式串的不适配字符的上一位字符前后缀相同的位置,
//求next数组,前缀与后缀不适配时,同样是移动模式串,只不过这个模式串是自身的前缀部分.
//而视频中的len恰好指向了前缀部分,但是要找自身的前一位 所以prefix【len-1】
/*
pattern[] 模式串
prefix[] 前缀表
n 模式串的长度
*/
void prefix_table(char pattern[], int prefix[], int n) {
prefix[0] = 0;//pattern[0] = 0;//第一个元素前后缀字串最大公共长度为0 next数组
int len = 0;//前后缀最大公共长度 负责跳转指针
int i = 1;//从下标为1的开始,即第二个字符 主指针,一直遍历模式串,不跳转
while (i < n) {//n是字符串长度
//相等
if (pattern[i] == pattern[len]) {
len++; //前后缀最大公共长度+1
prefix[i] = len;//此处 前后缀最大公共长度为 len
i++;//遍历下一个字符
}
//不相等的时候
else {
if (len > 0) {//避免数组越界
len = prefix[len - 1]; //len指针往后退,找与i指针指向的后缀字符串 能够与之匹配的后缀表达式 的最长前后缀最大公共长度
}
else {
//两个字符不相等,且不能再退了,再退就小于0了
prefix[i] = len;//此时 len一定为0;即 最长公共前后缀的长度为 0
i++;
}
}
}
}
//右移一位,便于操作 得到next数组
void move_prefix_table(int prefix[], int n) {
for (int i = n - 1; i > 0; --i) {
prefix[i] = prefix[i - 1];
}
prefix[0] = -1;
}
void kmp_search(char text[], char pattern[]) {
// text[i] len(text): m
// pattern[j] len(pattern): n
int m = strlen(text);
int n = strlen(pattern);
int* prefix = (int*)malloc(sizeof(int) * n);
prefix_table(pattern, prefix, n);
move_prefix_table(prefix, n);
int i = 0;
int j = 0;
while (i < m) {
if (j == n - 1 && text[i] == pattern[j]) {//全部匹配
printf("Found pattren at %d\n", i - j);
j = prefix[j];//找到字符串中所有匹配的位置 指向最长公共前后缀中前缀的子字符串的 下一位
if (n - j > m - i) // 若未比较的pattern长度已超出text剩下的长度,可提前结束比较
{
break;
}
}
//匹配
if (text[i] == pattern[j]) {
i++;
j++;
}
//没有匹配
else {
j = prefix[j];
if (j == -1)//j指向-1了,即指向了空白,空白当然和任何字符都不匹配,只能再往后移动了
{
i++;
j++;
}
}
}
}
int main() {
//char pattern[] = "ABABCABAA";
/*
int prefix[9];
int n = 9;
prefix_table(pattern,prefix,n);
int i ;
for( i = 0 ; i < n ; i++){
printf("%d\n",prefix[i]);
}
*/
//char pattern[] = " ABABCABAA";
//char text[] = "ABABABCABAAC";
char text[] = "centralchinanormaluniversityofchina";
char pattern[] = "china";
kmp_search(text, pattern);
return 0;
return 0;
}