在开始前需要了解子串和真子串的区别
abc 的 子串有 a ,b,c,ab,bc,ac,abc , 而真子串是不包括自身的其他子串
BF算法
目的 : 在主串中 ,找到子串开始的位置
如 主串 aaaabaa 子串 ab 就应该返回下标3
BF算法的思想是 :1让主串和子串一一比较 ,主串下标记作i 子串下标记作j 让i++ j++
2 如果j 走出了范围就说明 找到了子串 , 返回 i -j (因为要返回起始位置 ,主串走过的长度就是i)
3 如果 i 和 j 不相等,就让 j =0 , i 回退到这一趟 开始下标的下一个位置 (i-j+1),如果这里的 i 不回退的话,有可能跳过子串 。
如 主串是 aaabaaaa 子串是 aab , 应该返回的是 1 下标 ,但 如果没有回退 i 的话 , 开始比较 到 2 号下标不相等 i 又从 2下标开始比较 ,跳过了主串 。所以需要回退i 。
以上就是 BF算法的主要思想 , 下来实现一下 代码 。
int BF_Search(const char *str, const char *sub, int pos)//pos代表主串开始查找的下标位置 { assert(str!=NULL && sub!=NULL); if(pos<0 || pos>=(int)strlen(str)) { //return -1; pos = 0; } int len_str = strlen(str);//主串的长度信息 int len_sub = strlen(sub);//子串的长度信息 int i = pos;//主串开始位置 int j = 0;//子串开始位置 while(i<len_str && j<len_sub) { if(str[i] == sub[j])//如果相等,两者同时向后走,i++,j++ { i++; j++; } else//不相等 i回退到i-j+1 j回退开始位置0 { i = i-j+1;//i-j是这一趟的开始位置 这一趟的下一个位置:i-j+1 j = 0; } } //此时while循环退出 两种情况,要么i走出范围 要么j走出范围 if(j >= len_sub)//如果子串的j走出范围,找到了,返回i-j { return i-j; } else//否则没有找到,匹配失败,返回-1 { return -1; } }
BF算法的问题效率太低 , 时间复杂度是O(n*m)
效率低主要是因为 i 进行了太多次无效的回退
如 主串是 abcbcababcd ,子串是abcd ,这种情况下 ,在3 下标发生失配 ,还有必要让 i 回退到 1 下标吗 ,在子串各个字母不相等的情况下是没必要让 i 进行回退的 , 因为 对 abcd 来说 不管在哪个下标发生失配 前面相等的部分都跟第一个下标 a 不相等 , 回退都是无效操作 。
或者主串是 abcabdabdabcabc , 子串是abcabc, 在5 下标发生失配时 ,i 回退到b或者c 都是浪费时间的操作 ,因为和子串第一个字符都不相等 , 回退到 3 下标a 才算有效回退。
根据以上思路就有了KMP 算法
KMP 算法的核心思想是不让 i 回退
如果是第一种情况子串的字符互不相等 , 让 i ++ ,j ++ ,发生失配时 j =0 就可
重点是第二种情况子串有相等的字符,如何不让 i 回退 :对于 abcabdabdabcabc 和 abcabc 来说 ,i 在5 号下标发生失配 ,而它的合适回退位置是3下标 , 但仔细观察发现 3和4下标 和子串的前两个字符都相等,如果我们让 j 直接回退到 3 下标开始 那 i 就不用回退了。
我们把 j 回退的合适位置记作 k ,让 j 每次回退到合适位置 k 代替 i 的回退。而对于子串来说 , j有可能在任何位置发生失配 ,所以每个位置都要有一个合适回退位置 k 。定义一个next数组来记录k 。
找k 的方法 : 发生失配位置向前看,看子串中是否存在两个相等真子串,一个顶头一个顶尾,如果存在 ,k 就是这个真子串的长度。
因为前两个字符都不存在两个真子串 , 所以前两个k是固定的 -1和0 , 对于abcabc来说 它的next是 { -1,0,0,0,1,2} 。
如何理解找 k 的方法呢 ,找 k 的目的是让 j 代替 i 的回退 ,如果没有 k 就是让 i 退到有效适配的位置 ,让 j 退到 0进行比较 , 而 i 到有效适配中间的字符 就是 j 发生失配位置 之前的后半段 ,所以找顶头和顶尾的真子串 ,如果没有的话 那 j 失配位置的后半段 必定不和前半段相等 ,这个时候k 就是0 ,如果有相等的真子集 , 那 i 回退的那一段 就和 j 顶头的子集相等 ,所以让 j 从相等之后开始 就可以代替 i 的回退。
比如有子串 ababcababcaba ,如果是 4 下标 c 发生失配 ,对于主串来说应该回退到 3下标 a位置是有效回退 , 而 i 回退的位置 ab 又和 子串开始的ab 相等 ,所以让 i 不会退的话 , j 应该回退到 2下标。
//专门针对子串获取其next数组 int *Get_next(const char *sub) { //assert int len_sub = strlen(sub); int *next = (int*)malloc(sizeof(int) * len_sub); assert(next != 0); next[0] = -1; next[1] = 0; int j = 1; int k = 0; //通过已知推位置 j是已知 则j+1是未知 while(j+1 < len_sub)//未知位置需要合法 所以做了一个判断 { if(sub[j] == sub[k] || (k==-1))//要么相等k++赋值,要么不相等k一直回退,触发了保底机制(k==-1) { //next[++j] = ++k; k++; j++; next[j] = k; } else { k = next[k]; } } return next; } int KMP_Search(const char *str, const char *sub, int pos)//pos代表主串开始查找的下标位置 { assert(str!=NULL && sub!=NULL); if(pos<0 || pos>=(int)strlen(str)) { //return -1; pos = 0; } int len_str = strlen(str);//主串的长度信息 int len_sub = strlen(sub);//子串的长度信息 int i = pos;//主串开始位置 int j = 0;//子串开始位置 int *next = Get_next(sub); while(i<len_str && j<len_sub) { if((j==-1) || str[i] == sub[j])//如果相等,两者同时向后走,i++,j++ { i++; j++; } else//不相等 i回退到i-j+1 j回退开始位置0 { //i不回退 j = next[j];//next[j] == k } } //此时while循环退出 两种情况,要么i走出范围 要么j走出范围 if(j >= len_sub)//如果子串的j走出范围,找到了,返回i-j { return i-j; } else//否则没有找到,匹配失败,返回-1 { return -1; } }