面试题: 判断字符串是否在另外一个字符串中存在?
面试时发现好多人回答很差, 因此就梳理了一下已知的方法, 此文较长, 须要耐心的看下去。从实现和算法原理两方面解此问题, 其中有用PHP原生方法实现也有一些业界大牛创造的算法。php
<?php /* strpos示例 */ // test echo 'match:', strpos('xasfsdfbk', 'xasfsdfbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', strpos('xasfsdfbk', 'fbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', strpos('xasfsdfbk', 'xs') != false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', strpos('xasfsdfbk', 'sfs') !== false ? 'true' : 'false', ';', PHP_EOL; // code strpos('xasfsdfbk', 'sfs'); // mb* 相关的函数也可, 好比说mb_strpos是基于字符数执行一个多字节安全的 strpos() 操做。
函数 | 描述 | 版本 |
---|---|---|
strpos | 查找字符串首次出现的位置 | PHP 4, PHP 5, PHP 7 |
stripos | 查找字符串首次出现的位置(不区分大小写) | PHP 5, PHP 7 |
strrpos | 计算指定字符串在目标字符串中最后一次出现的位置 | PHP 4, PHP 5, PHP 7 |
strripos | 计算指定字符串在目标字符串中最后一次出现的位置(不区分大小写) | PHP 5, PHP 7 |
mb_strpos | 查找字符串在另外一个字符串中首次出现的位置 | PHP 4 >= 4.0.6, PHP 5, PHP 7 |
strstr | 查找字符串的首次出现 | PHP 4, PHP 5, PHP 7 |
stristr | strstr() 函数的忽略大小写版本 | PHP 4, PHP 5, PHP 7 |
substr_count | 计算字串出现的次数 | PHP 4, PHP 5, PHP 7 |
<?php // test echo 'match:', str_match('xasfsdfbk', 'xasfsdfbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'fbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'xs') != false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'sfs') !== false ? 'true' : 'false', ';', PHP_EOL; // code function str_match($a, $b) { return preg_match('/' . $b . '/i', $a, $matchs) ? true : false; }
函数 | 描述 | 版本 |
---|---|---|
preg_match | 执行匹配正则表达式 | PHP 4, PHP 5, PHP 7 |
preg_match_all | 执行一个全局正则表达式匹配 | PHP 4, PHP 5, PHP 7 |
<?php // test echo 'match:', str_match('xasfsdfbk', 'xasfsdfbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'fbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'xs') != false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'sfs') !== false ? 'true' : 'false', ';', PHP_EOL; // code function str_match($a, $b) { return count(explode($b, $a)) >= 2 ? true : false; } // strtok 能够么? // 在分割字符串时,split()与explode()谁快?
函数 | 描述 | 版本 |
---|---|---|
strtok | 标记分割字符串 | PHP 4, PHP 5, PHP 7 |
explode | 使用一个字符串分割另外一个字符串 | PHP 4, PHP 5, PHP 7 |
split | 用正则表达式将字符串分割到数组中 | PHP 4, PHP 5 |
mb_split | 使用正则表达式分割多字节字符串 | PHP 4 >= 4.2.0, PHP 5, PHP 7 |
preg_split | 经过一个正则表达式分隔字符串 | PHP 4, PHP 5, PHP 7 |
<?php // test echo 'match:', str_match('xasfsdfbk', 'xasfsdfbk') !== -1 ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'fbk') !== -1 ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'xs') !== -1 ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'sfs') !== -1 ? 'true' : 'false', ';', PHP_EOL; // code function str_match($a, $b) { if($a == $b) { return 0; } $aArr = str_split($a); $bArr = str_split($b); $aLen = count($aArr); $bLen = count($bArr); if($bLen > $aLen) { return -1; } $data = []; $aLastIndex = -1; $bStartIndex = 0; for($ai = 0; $ai < $aLen; $ai++) { $av = $aArr[$ai]; $exists = false; for($bi = $bStartIndex; $bi < $bLen; $bi++) { $bv = $bArr[$bi]; if(($aLastIndex == -1 || $ai == ($aLastIndex + 1)) && $av == $bv) { $exists = true; break; } if ($aLastIndex != -1 && $ai == ($aLastIndex + 1) && $av != $bv) { break; } } if ($exists) { $aLastIndex = $ai; $bStartIndex = $bi + 1; array_push($data, [ 'value' => $av, 'index' => $ai ]); } else { $aLastIndex = -1; $bStartIndex = 0; $data = []; } if ($exists && $bLen == $bStartIndex) { break; } } if(!empty($data) && count($data) == $bLen) { $begin = array_shift($data); return $begin['index']; } else { return -1; } }
<?php // demo echo 'match:', str_match('xasfsdfbk', 'xasfsdfbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'fbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'xs') != false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'sfs') !== false ? 'true' : 'false', ';', PHP_EOL; // code function str_match($a, $b) { $aArr = str_split($a); $bArr = str_split($b); $aLen = count($aArr); $bLen = count($bArr); for ($ai = 0; $ai <= $aLen - $bLen; $ai++) { for ($bi = 0; $bi < $bLen; $bi++) { if($aArr[$ai + $bi] != $bArr[$bi]) { break; } } if($bLen == $bi) { return $ai; } } return false; }
#include <iostream> #include <string.h> using namespace std; #define BASE 256 #define MODULUS 101 void RabinKarp(char t[], char p[]) { int t_len = strlen(t); int p_len = strlen(p); // 哈希滚动之用 int h = 1; for (int i = 0; i < p_len - 1; i++) h = (h * BASE) % MODULUS; int t_hash = 0; int p_hash = 0; for (int i = 0; i < p_len; i++) { t_hash = (BASE * t_hash + t[i]) % MODULUS; p_hash = (BASE * p_hash + p[i]) % MODULUS; } int i = 0; while (i <= t_len - p_len) { // 考虑到哈希碰撞的可能性,还须要用 memcmp 再比对一下 if (t_hash == p_hash && memcmp(p, t + i, p_len) == 0) cout << p << " is found at index " << i << endl; // 哈希滚动 t_hash = (BASE * (t_hash - t[i] * h) + t[i + p_len]) % MODULUS; // 防止出现负数 if (t_hash < 0) t_hash = t_hash + MODULUS; i++; } } int main() { char t[100] = "It is a test, but not just a test"; char p[10] = "test"; RabinKarp(t, p); return 0; }
<?php // php 实现 function hash_string($str, $len) { $hash = ''; $hash_table = array( 'h' => 1, 'e' => 2, 'l' => 3, 'o' => 4, 'w' => 5, 'r' => 6, 'd' => 7, ); for ($i = 0; $i < $len; $i++) { $hash .= $hash_table[$str{$i}]; } return (int)$hash; } function rabin_karp($text, $pattern) { $n = strlen($text); $m = strlen($pattern); $text_hash = hash_string(substr($text, 0, $m), $m); $pattern_hash = hash_string($pattern, $m); for ($i = 0; $i < $n-$m+1; $i++) { if ($text_hash == $pattern_hash) { return $i; } $text_hash = hash_string(substr($text, $i, $m), $m); } return -1; } // 2 echo rabin_karp('hello world', 'ello');
public class KMP { public static int KMPSearch(String txt, String pat, int[] next) { int M = txt.length(); int N = pat.length(); int i = 0; int j = 0; while (i < M && j < N) { if (j == -1 || txt.charAt(i) == pat.charAt(j)) { i++; j++; } else { j = next[j]; } } if (j == N) return i - j; else return -1; } public static void getNext(String pat, int[] next) { int N = pat.length(); next[0] = -1; int k = -1; int j = 0; while (j < N - 1) { if (k == -1 || pat.charAt(j) == pat.charAt(k)) { ++k; ++j; next[j] = k; } else k = next[k]; } } public static void main(String[] args) { String txt = "BBC ABCDAB CDABABCDABCDABDE"; String pat = "ABCDABD"; int[] next = new int[pat.length()]; getNext(pat, next); System.out.println(KMPSearch(txt, pat, next)); } }
public class BoyerMoore { public static void getRight(String pat, int[] right) { for (int i = 0; i < 256; i++){ right[i] = -1; } for (int i = 0; i < pat.length(); i++) { right[pat.charAt(i)] = i; } } public static int BoyerMooreSearch(String txt, String pat, int[] right) { int M = txt.length(); int N = pat.length(); int skip; for (int i = 0; i <= M - N; i += skip) { skip = 0; for (int j = N - 1; j >= 0; j--) { if (pat.charAt(j) != txt.charAt(i + j)) { skip = j - right[txt.charAt(i + j)]; if (skip < 1){ skip = 1; } break; } } if (skip == 0) return i; } return -1; } public static void main(String[] args) { String txt = "BBC ABCDAB AACDABABCDABCDABDE"; String pat = "ABCDABD"; int[] right = new int[256]; getRight(pat,right); System.out.println(BoyerMooreSearch(txt, pat, right)); } }
public class Sunday { public static int getIndex(String pat, Character c) { for (int i = pat.length() - 1; i >= 0; i--) { if (pat.charAt(i) == c) return i; } return -1; } public static int SundaySearch(String txt, String pat) { int M = txt.length(); int N = pat.length(); int i, j; int skip = -1; for (i = 0; i <= M - N; i += skip) { for (j = 0; j < N; j++) { if (txt.charAt(i + j) != pat.charAt(j)){ if (i == M - N) break; skip = N - getIndex(pat, txt.charAt(i + N)); break; } } if (j == N) return i; } return -1; } public static void main(String[] args) { String txt = "BBC ABCDAB AACDABABCDABCDABD"; String pat = "ABCDABD"; System.out.println(SundaySearch(txt, pat)); } }
#include <stdio.h> #include <stdlib.h> #include <string.h> int index_bf(char *s,char *t,int pos); int index_bf_self(char *s,char *t,int index); int main() { char s[]="6he3wor"; //标准BF算法中,s[0]和t[0]存放的为字符串长度。 char t[]="3wor"; int m=index_bf(s,t,2); //标准BF算法 printf("index_bf:%d\n",m); m=index_bf_self(s,t,2); //修改版BF算法,s和t中,没必要再存放字符串长度。 printf("index_bf_self:%d\n",m); exit(0); } /* 字符串S和T中,s[0],t[0]存放必须为字符串长度 例:s[]="7hi baby!" T[]="4baby" index_bf(s,t,1); pos:在S中要从下标pos处开始查找T (说明:标准BF算法中,为研究方便,s[0],t[0]中存放的为各自字符串长度。) */ int index_bf(char *s,char *t,int pos) { int i,j; if(pos>=1 && pos <=s[0]-'0') { i=pos; j=1; while(i<=s[0]-'0'&&j<=t[0]-'0') { if(s[i]==t[j]) { i++; j++; } else { j=1; i=i-j+2; } if(j>t[0]-'0') { return i-t[0]+'0'; } } return -1; } else { return -1; } } /* 修改版,字符串s和t中,没必要再包含字符串长度。 例:s[]="hi baby" t[]="baby" index_bf_self(s,t,0); index:在s中,从几号下标开始查找 */ int index_bf_self(char *s,char *t,int index) { int i=index,j=0; while(s[i]!='\0') { while(*(t+j)!='\0' && *(s+i+j)!='\0') { if(*(t+j)!=*(s+i+j)) break; j++; } if(*(t+j)=='\0') { return i; } i++; j=0; } return -1; }
//////////////////////////////////////////////////// /* 程序说明:多模式串匹配的AC自动机算法 自动机算法能够参考《柔性字符串匹配》里的相应章节,讲的很清楚 */ #include <stdio.h> #include <string.h> const int MAXQ = 500000+10; const int MAXN = 1000000+10; const int MAXK = 26; //自动机里字符集的大小 struct TrieNode { TrieNode* fail; TrieNode* next[MAXK]; bool danger; //该节点是否为某模式串的终结点 int cnt; //以该节点为终结点的模式串个数 TrieNode() { fail = NULL; memset(next, NULL, sizeof(next)); danger = false; cnt = 0; } }*que[MAXQ], *root; //文本字符串 char msg[MAXN]; int N; void TrieInsert(char *s) { int i = 0; TrieNode *ptr = root; while(s[i]) { int idx = s[i]-'a'; if(ptr->next[idx] == NULL) ptr->next[idx] = new TrieNode(); ptr = ptr->next[idx]; i++; } ptr->danger = true; ptr->cnt++; } void Init() { int i; char s[100]; root = new TrieNode(); scanf("%d", &N); for(i = 0; i < N; i++) { scanf("%s", s); TrieInsert(s); } } void Build_AC_Automation() { int rear = 1, front = 0, i; que[0] = root; root->fail = NULL; while(rear != front) { TrieNode *cur = que[front++]; for(i = 0; i < 26; i++) if(cur->next[i] != NULL) { if(cur == root) cur->next[i]->fail = root; else { TrieNode *ptr = cur->fail; while(ptr != NULL) { if(ptr->next[i] != NULL) { cur->next[i]->fail = ptr->next[i]; if(ptr->next[i]->danger == true) cur->next[i]->danger = true; break; } ptr = ptr->fail; } if(ptr == NULL) cur->next[i]->fail = root; } que[rear++] = cur->next[i]; } } } int AC_Search() { int i = 0, ans = 0; TrieNode *ptr = root; while(msg[i]) { int idx = msg[i]-'a'; while(ptr->next[idx] == NULL && ptr != root) ptr = ptr->fail; ptr = ptr->next[idx]; if(ptr == NULL) ptr = root; TrieNode *tmp = ptr; while(tmp != NULL && tmp->cnt != -1) { ans += tmp->cnt; tmp->cnt = -1; tmp = tmp->fail; } i++; } return ans; } int main() { int T; scanf("%d", &T); while(T--) { Init(); Build_AC_Automation(); //文本 scanf("%s", msg); printf("%d\n", AC_Search()); } return 0; }
/* 字符串转数组, 取交集, 判断结果 */ // demo echo 'match:', str_match('xasfsdfbk', 'xasfsdfbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'fbk') !== false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'xs') != false ? 'true' : 'false', ';', PHP_EOL; echo 'match:', str_match('xasfsdfbk', 'sfs') !== false ? 'true' : 'false', ';', PHP_EOL; // code function str_match($a, $b) { $aArr = str_split($a); $bArr = str_split($b); return join('', array_intersect($aArr, $bArr)) == $b; } // 集合中的元素具备惟一性, 被匹配的字符串中有相同的字符, 将会去重 // 不能保证交集后的元素顺序连续
Rabin-Karp 算法(也能够叫 Karp-Rabin 算法),由 Richard M. Karp 和 Michael O. Rabin 在 1987 年发表,它也是用来解决多模式串匹配问题的。html
它的实现方式有点不同凡响,首先是计算两个字符串的哈希值,而后经过比较这两个哈希值的大小来判断是否出现匹配。java
选择一个合适的哈希函数很重要。假设文本串为t[0, n)
,模式串为p[0, m)
,其中 0<m<n,Hash(t[i,j])
表明字符串t[i, j]
的哈希值。ios
当 Hash(t[0, m-1])!=Hash(p[0,m-1])
时,咱们很天然的会把 Hash(t[1, m])
拿过来继续比较。在这个过程当中,若咱们从新计算字符串t[1, m]
的哈希值,还须要 O(n)
的时间复杂度,不划算。观察到字符串t[0, m-1]
与t[1, m]
中有 m-1
个字符是重合的,所以咱们能够选用滚动哈希函数,那么从新计算的时间复杂度就降为 O(1)
。git
Rabin-Karp 算法选用的滚动哈希函数主要是利用 Rabin fingerprint 的思想,举个例子,计算字符串t[0, m - 1]的哈希值的公式以下,github
Hash(t[0,m-1]) = t[0]*bm-1 + t[1]*bm-2 + ... + t[m-1]*b0
其中的 b 是一个常数,在 Rabin-Karp 算法中,咱们通常取值为 256,由于一个字符的最大值不超过 255。上面的公式还有一个问题,哈希值若是过大可能会溢出,所以咱们还须要对其取模,这个值应该尽量大,且是质数,这样能够减少哈希碰撞的几率,在这里咱们就取 101。面试
则计算字符串t[1, m]的哈希值公式以下,正则表达式
Hash(t[1,m]) = ( Hash(t[0,m−1]) − t[0]∗bm−1 ) ∗ b + t[m]∗b0
如图, 算法导论上提供的示例图:算法
许多算法能够完成这个任务,Knuth-Morris-Pratt算法(简称KMP)是最经常使用的之一。它以三个发明者命名,起头的那个K就是著名科学家Donald Knuth。数组
这种算法不太容易理解,网上有不少解释,但读起来都很费劲。直到读到Jake Boxer的文章,才真正理解这种算法。下面是阮一峰对KMP算法解释。
9.已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,所以按照下面的公式算出向后移动的位数:
移动位数 = 已匹配的字符数 - 对应的部分匹配值
由于 6 - 2 等于4,因此将搜索词向后移动4位。
首先,要了解两个概念:"前缀"和"后缀"。
"前缀"指除了最后一个字符之外,一个字符串的所有头部组合;"后缀"指除了第一个字符之外,一个字符串的所有尾部组合。
概念 | 描述 | 字符串示例 "bread" |
---|---|---|
前缀 | 除了最后一个字符之外,一个字符串的所有头部组合 | b, br, bre, brea |
后缀 | 除了第一个字符之外,一个字符串的所有尾部组合 | read, ead, ad, d |
"部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,
字符串 | 前缀 | 后缀 | 共有元素 | 共有元素长度 |
---|---|---|---|---|
A | [] | [] | [] | 0 |
AB | [A] | [B] | [] | 0 |
ABC | [A, AB] | [BC, C] | [] | 0 |
ABCD | [A, AB, ABC] | [BCD, CD, D] | [] | 0 |
ABCDA | [A, AB, ABC, ABCD] | [BCDA, CDA, DA, A] | [A] | 1 |
ABCDAB | [A, AB, ABC, ABCD, ABCDA] | [BCDAB, CDAB, DAB, AB, B] | [AB] | 2 |
ABCDABD | [A, AB, ABC, ABCD, ABCDA, ABCDAB] | [BCDABD, CDABD, DABD, ABD, BD, D] | [] | 0 |
KMP算法并非效率最高的算法,实际采用并很少。各类文本编辑器的"查找"功能(Ctrl+F),大多采用Boyer-Moore算法。
Boyer-Moore算法不只效率高,并且构思巧妙,容易理解。1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了这种算法。
下面是阮一峰根据Moore教授的例子对Boyer-Moore算法的解释。
这是一个很聪明的想法,由于若是尾部字符不匹配,那么只要一次比较,就能够知道前7个字符(总体上)确定不是要找的结果。
咱们看到,"S"与"E"不匹配。这时,"S"就被称为"坏字符"(bad character),即不匹配的字符。咱们还发现,"S"不包含在搜索词"EXAMPLE"之中,这意味着能够把搜索词直接移到"S"的后一位。
后移位数 = 坏字符的位置 - 搜索词中的上一次出现位置
若是"坏字符"不包含在搜索词之中,则上一次出现位置为 -1。
以"P"为例,它做为"坏字符",出如今搜索词的第6位(从0开始编号),在搜索词中的上一次出现位置为4,因此后移 6 - 4 = 2位。再之前面第二步的"S"为例,它出如今第6位,上一次出现位置是 -1(即未出现),则整个搜索词后移 6 - (-1) = 7位。
后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置
举例来讲,若是字符串"ABCDAB"的后一个"AB"是"好后缀"。那么它的位置是5(从0开始计算,取最后的"B"的值),在"搜索词中的上一次出现位置"是1(第一个"B"的位置),因此后移 5 - 1 = 4位,前一个"AB"移到后一个"AB"的位置。
再举一个例子,若是字符串"ABCDEF"的"EF"是好后缀,则"EF"的位置是5 ,上一次出现的位置是 -1(即未出现),因此后移 5 - (-1) = 6位,即整个字符串移到"F"的后一位。
这个规则有三个注意点:
回到上文的这个例子。此时,全部的"好后缀"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"还出如今头部,因此后移 6 - 0 = 6位。
12.能够看到,"坏字符规则"只能移3位,"好后缀规则"能够移6位。因此,Boyer-Moore算法的基本思想是,每次后移这两个规则之中的较大值。
更巧妙的是,这两个规则的移动位数,只与搜索词有关,与原字符串无关。所以,能够预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就能够了。
Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很类似:1
只不过Sunday算法是从前日后匹配,在匹配失败时关注的是主串中参加匹配的最末位字符的下一位字符。
若是该字符没有在模式串中出现则直接跳过,即移动位数 = 模式串长度 + 1;
不然,其移动位数 = 模式串长度 - 该字符最右出现的位置(以0开始) = 模式串中该字符最右出现的位置到尾部的距离 + 1。
下面举个例子说明下Sunday算法。假定如今要在主串”substring searching”中查找模式串”search”。
刚开始时,把模式串与文主串左边对齐:
结果发如今第2个字符处发现不匹配,不匹配时关注主串中参加匹配的最末位字符的下一位字符,即标粗的字符 i,由于模式串search中并不存在i,因此模式串直接跳过一大片,向右移动位数 = 匹配串长度 + 1 = 6 + 1 = 7,从 i 以后的那个字符(即字符n)开始下一步的匹配,以下图:
结果第一个字符就不匹配,再看主串中参加匹配的最末位字符的下一位字符,是’r’,它出如今模式串中的倒数第3位,因而把模式串向右移动3位(m - 3 = 6 - 3 = r 到模式串末尾的距离 + 1 = 2 + 1 =3),使两个’r’对齐,以下:
匹配成功。
回顾整个过程,咱们只移动了两次模式串就找到了匹配位置,缘于Sunday算法每一步的移动量都比较大,效率很高。
BF算法核心思想是:首先S[1]和T[1]比较,若相等,则再比较S[2]和T[2],一直到T[M]为止;若S[1]和T[1]不等,则T向右移动一个字符的位置,再依次进行比较。若是存在k,1≤k≤N,且S[k+1…k+M]=T[1…M],则匹配成功;不然失败。该算法最坏状况下要进行M(N-M+1)次比较,时间复杂度为O(MN)。下面结合图片,解释一下:
S表明源字符串,T表明咱们要查找的字符串。BF算法能够表述以下:依次遍历字符串S,看是否字符串S中含有字符串T。
所以,咱们依次比较S[0] 和T[0]、S[1] 和T[1]、S[2] 和T[2]……S[n]和T[n] ,从图中咱们可知,S[0]-S[7]和T[0]-T[7]依次相等。当匹配到S[8]和T[8]时,两个字符不等。根据定义,此时S和T都要回溯,T向右移动一个字符的位置,即S回溯到S[1]的位置,T回溯到T[0]的位置,再从新开始比较。此时,S[1]和T[0]、S[2]和T[1]……若是再次发现不匹配字符,则再次回溯,即S回溯到S[2]的位置,T回到T[0]的位置。循环往复,直到到达S或者T字符串的结尾。若是是到达S串的结尾,则表示匹配失败,若是是到达T串的结尾,则表示匹配成功。
BF算法优势:思想简单,直接,无需对字符串S和T进行预处理。缺点:每次字符不匹配时,都要回溯到开始位置,时间开销大。
Aho-Corasick算法又叫AC自动机算法,是一种多模式匹配算法。Aho-Corasick算法能够在目标串查找多个模式串,出现次数以及出现的位置。
Aho-Corasick算法主要是应用有限自动机的状态转移来模拟字符的比较,下面对有限状态机作几点说明
上图是由多模式串{he,she,his,hers}构成的一个有限状态机:
1.该状态当字符匹配是按实线标注的状态进行转换,当全部实线路径都不知足(即下一个字符都不匹配时)按虚线状态进行转换。
2.对ushers匹配过程以下图所示:
当转移到红色结点时表示已经匹配而且得到模式串
Aho-Corasick算法步骤
Aho-Corasick算法和前面的算法同样都要对模式串进行预处理,预处理主要包括字典树Tire的构造,构建状态转移表(goto),失效函数(failure function),输出表(Output)。
Aho-Corasick算法包括如下3个步骤
下面3个步骤分别进行介绍
Tire是哈希树的变种,Tire树的边是模式串的字符,结点就是Tire的状态表,下图是多模式串{he,she,his,hers}的Tire树结构:
下面是多模式串{he,she,his,hers}的goto函数,failure函数,output函数
函数 | 结构图 |
---|---|
goto函数 | ![]() |
failure函数 | ![]() |
output函数 | ![]() |
通常而言,好的字符串匹配算法要有如下特色:
这是评价一个字符匹配算法最重要的标准。一般要求字符匹配能以线性速度执行。
序号 | 指标 | 描述 |
---|---|---|
1) | 预处理时间的复杂性 | 有些算法在进行字符串匹配前须要对模式特征进行预处理 |
2) | 匹配阶段的时间复杂性 | 字符串匹配过程当中执行查找操做的时间复杂性,它一般和文本长度及模式长度相关 |
3) | 最坏状况下的时间复杂性 | 对一个text进行字符模式匹配时,设法下降各算法的最坏状况下的时间复杂性是目前的研究热点之一 |
4) | 最好状况下的时间复杂性 | 对一个text进行字符模式匹配时的最好的可能性。 |
执行预处理和模式匹配不只须要CPU资源还须要内存资源,尽管目前内存的容量较之前大得多,但为了提升速度,人们常利用特殊硬件。一般,特殊硬件中内存访问速度很快但容量偏小,这时,占用资源少的算法将更具优点。