字符串的匹配先定义两个名词:模式串和文本串。我们的任务就是在文本串中找到模式串第一次出现的位置,如果找到就返回位置的下标,如果没有找到返回-1.其实这就是C++语言里面的一个函数:
extern char *strstr(char *str1, const char *str2);
对于这个函数的解释:
str1: 被查找目标 str2: 要查找对象 返回值:如果str2是str1的子串,则返回str2在str1的首次出现的地址; 如果str2不是str1的子串,则返回NULL。 例如: char str[]="1234xyz"; char *str1=strstr(str,"34"); cout << str1 << endl; 显示的是: 34xyz
返回值是一个指针,这个指针指向文本串中第一次出现模式串的位置。
字符串查找的暴力算法
先看LeetCode上的一道题目,实现这个函数 int strStr(string haystack, string needle); ,要求返回文本串中出现模式串的下标值。
1.如果模式串为NULL,那么直接返回0. 2.如果模式串的长度大于文本串,那么一定查找不到,返回-1. 3.如果存在的话,查找的范围可以限定在文本串的0~s.size()-p.size();
所以暴力算法的代码实现:
int strStr(string haystack, string needle) { int i = 0; //模式串为空 if(needle.empty()) { return 0; } //文本串的大小小于模式串 if(haystack.size() < needle.size()) { return -1; } //确定查找的范围 for(i = 0; i <= haystack.size()-needle.size(); ++i) { int j = 0; for(j = 0; j < needle.size(); ++j) { if(haystack[i+j] != needle[j]) { break; } }//for if(j == needle.size()) { return i; } }//for if(i == haystack.size()-needle.size() + 1) { return -1; } }
字符串查找的KMP算法
上面的暴力算法,在查找失败以后都要进行回溯,下面再给出一个版本,明显的看到i,j的回溯:
int strStr(string haystack, string needle) { int sLen = haystack.size(); int pLen = needle.size(); int i = 0; int j = 0; while(i < sLen && j < pLen) { if(haystack[i] == needle[j]) { ++i; ++j; } else { i = i - j + 1; j = 0; } }//while if(j == pLen) { return i - j; } else { return -1; } }
假设我们已经知道了KMP的next数组,所以每次失配以后,i不回溯,j回溯到next[j]指定的位置。也就是 j = next[j]; 。
int KmpSearch(char *s, char *p) { int i = 0; int j = 0; int sLen = strlen(s); int pLen = strlen(p); while(i < sLen && j < pLen) { //如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++ if(j == -1 || s[i] == p[j]) { i++; j++; } else { //如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j] //next[j]即为j所对应的next值 j = next[j]; } }//while if(j == pLen) { return i - j; } else { return -1; } }
对于上面的j=-1和next[0]=-1作下面的解释:
说完了KMP的大的框架,下面就得说一下next数组的求解过程了:
void GetNext(char *p, int *next) { int pLen = strlen(p); int j = 0; int k = -1; next[0] = -1; while(j < pLen - 1) { //p[k]表示前缀,p[j]表示后缀 if(k == -1 || p[j] == p[k]) { ++k; ++j; next[j] = k;//表示在j这个字符之前,能够构成公共前后缀的最大字符数 } else { k = next[k];//回溯之前已经有过匹配的前缀 } } }
KMP算法的一个改进
上面的KMP算法已经能够很好的跑出结果来了,但是还可以改进,看下面的一个字符串的匹配:
改进的代码实现:
void GetNextVal(char *p, int *next) { int pLen = strlen(p); int j = 0; int k = -1; next[0] = -1; while(j < pLen - 1) { //p[k]表示前缀,p[j]表示后缀 if(k == -1 || p[j] == p[k]) { ++k; ++j; if(p[j] != p[k]) { next[j] = k;//表示在j这个字符之前,能够构成公共前后缀的最大字符数 } else { next[j] = next[k];//因为不能出现p[j] = p[next[j]],所以当出现时需要继续递归,k = next[k] = next[next[k]] } } else { k = next[k];//回溯之前已经有过匹配的前缀 } } }
再来看一个极端的情况: