字符串查找算法及原理

面試題: 判斷字符串是否在另外一個字符串中存在?

面試時發現好多人回答很差, 因此就梳理了一下已知的方法, 此文較長, 須要耐心的看下去。從實現和算法原理兩方面解此問題, 其中有用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;
}

方法六: 字符串查找算法-Rabin-Karp算法

#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');

方法七: 字符串查找算法-KMP

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));
    }
}

方法八: 字符串查找算法-Boyer-Moore

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));
    }
}

方法九: 字符串查找算法-Sunday

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));
    }
}

方法十: 字符串查找算法-BF算法(Brute Force)

#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;  
}

方法十一: Aho-Corasick 算法

////////////////////////////////////////////////////    
/*  
程序說明:多模式串匹配的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)

image

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

如圖, 算法導論上提供的示例圖:
image算法

算法二: KMP算法(Knuth Morris Pratt)

許多算法能夠完成這個任務,Knuth-Morris-Pratt算法(簡稱KMP)是最經常使用的之一。它以三個發明者命名,起頭的那個K就是著名科學家Donald Knuth。數組

這種算法不太容易理解,網上有不少解釋,但讀起來都很費勁。直到讀到Jake Boxer的文章,才真正理解這種算法。下面是阮一峯對KMP算法解釋。

  1. 首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一個字符與搜索詞"ABCDABD"的第一個字符,進行比較。由於B與A不匹配,因此搜索詞後移一位。

image

  1. 由於B與A不匹配,搜索詞再日後移。

image

  1. 就這樣,直到字符串有一個字符,與搜索詞的第一個字符相同爲止。

image

  1. 接着比較字符串和搜索詞的下一個字符,仍是相同。

image

  1. 直到字符串有一個字符,與搜索詞對應的字符不相同爲止。

image

  1. 這時,最天然的反應是,將搜索詞整個後移一位,再從頭逐個比較。這樣作雖然可行,可是效率不好,由於你要把"搜索位置"移到已經比較過的位置,重比一遍。

image

  1. 一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是"ABCDAB"。KMP算法的想法是,設法利用這個已知信息,不要把"搜索位置"移回已經比較過的位置,繼續把它向後移,這樣就提升了效率。

image

  1. 怎麼作到這一點呢?能夠針對搜索詞,算出一張《部分匹配表》(Partial Match Table)。這張表是如何產生的,後面再介紹,這裏只要會用就能夠了。

image

9.已知空格與D不匹配時,前面六個字符"ABCDAB"是匹配的。查表可知,最後一個匹配字符B對應的"部分匹配值"爲2,所以按照下面的公式算出向後移動的位數:

移動位數 = 已匹配的字符數 - 對應的部分匹配值

由於 6 - 2 等於4,因此將搜索詞向後移動4位。
image

  1. 由於空格與C不匹配,搜索詞還要繼續日後移。這時,已匹配的字符數爲2("AB"),對應的"部分匹配值"爲0。因此,移動位數 = 2 - 0,結果爲 2,因而將搜索詞向後移2位。

image

  1. 由於空格與A不匹配,繼續後移一位。

image

  1. 逐位比較,直到發現C與D不匹配。因而,移動位數 = 6 - 2,繼續將搜索詞向後移動4位。

image

  1. 逐位比較,直到搜索詞的最後一位,發現徹底匹配,因而搜索完成。若是還要繼續搜索(即找出所有匹配),移動位數 = 7 - 0,再將搜索詞向後移動7位,這裏就再也不重複了。

image

  1. 下面介紹《部分匹配表》是如何產生的。

image

首先,要了解兩個概念:"前綴"和"後綴"。
"前綴"指除了最後一個字符之外,一個字符串的所有頭部組合;"後綴"指除了第一個字符之外,一個字符串的所有尾部組合。

概念 描述 字符串示例 "bread"
前綴 除了最後一個字符之外,一個字符串的所有頭部組合 b, br, bre, brea
後綴 除了第一個字符之外,一個字符串的所有尾部組合 read, ead, ad, d
  1. 部分匹配值實例

image

"部分匹配值"就是"前綴"和"後綴"的最長的共有元素的長度。以"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
  1. "部分匹配"的實質是,有時候,字符串頭部和尾部會有重複。好比,"ABCDAB"之中有兩個"AB",那麼它的"部分匹配值"就是2("AB"的長度)。搜索詞移動的時候,第一個"AB"向後移動4位(字符串長度-部分匹配值),就能夠來到第二個"AB"的位置。

image

算法三: Boyer-Moore算法

KMP算法並非效率最高的算法,實際採用並很少。各類文本編輯器的"查找"功能(Ctrl+F),大多采用Boyer-Moore算法。
image

Boyer-Moore算法不只效率高,並且構思巧妙,容易理解。1977年,德克薩斯大學的Robert S. Boyer教授和J Strother Moore教授發明了這種算法。

下面是阮一峯根據Moore教授的例子對Boyer-Moore算法的解釋。

  1. 假定字符串爲"HERE IS A SIMPLE EXAMPLE",搜索詞爲"EXAMPLE"。

image

  1. 首先,"字符串"與"搜索詞"頭部對齊,從尾部開始比較。

這是一個很聰明的想法,由於若是尾部字符不匹配,那麼只要一次比較,就能夠知道前7個字符(總體上)確定不是要找的結果。

咱們看到,"S"與"E"不匹配。這時,"S"就被稱爲"壞字符"(bad character),即不匹配的字符。咱們還發現,"S"不包含在搜索詞"EXAMPLE"之中,這意味着能夠把搜索詞直接移到"S"的後一位。

image

  1. 依然從尾部開始比較,發現"P"與"E"不匹配,因此"P"是"壞字符"。可是,"P"包含在搜索詞"EXAMPLE"之中。因此,將搜索詞後移兩位,兩個"P"對齊。

image

  1. 咱們由此總結出"壞字符規則":
後移位數 = 壞字符的位置 - 搜索詞中的上一次出現位置

若是"壞字符"不包含在搜索詞之中,則上一次出現位置爲 -1。

以"P"爲例,它做爲"壞字符",出如今搜索詞的第6位(從0開始編號),在搜索詞中的上一次出現位置爲4,因此後移 6 - 4 = 2位。再之前面第二步的"S"爲例,它出如今第6位,上一次出現位置是 -1(即未出現),則整個搜索詞後移 6 - (-1) = 7位。
image

  1. 依然從尾部開始比較,"E"與"E"匹配。

image

  1. 比較前面一位,"LE"與"LE"匹配。

image

  1. 比較前面一位,"PLE"與"PLE"匹配。

image

  1. 比較前面一位,"MPLE"與"MPLE"匹配。咱們把這種狀況稱爲"好後綴"(good suffix),即全部尾部匹配的字符串。注意,"MPLE"、"PLE"、"LE"、"E"都是好後綴。

image

  1. 比較前一位,發現"I"與"A"不匹配。因此,"I"是"壞字符"。

image

  1. 根據"壞字符規則",此時搜索詞應該後移 2 - (-1)= 3 位。問題是,此時有沒有更好的移法?

image

  1. 咱們知道,此時存在"好後綴"。因此,能夠採用"好後綴規則":
後移位數 = 好後綴的位置 - 搜索詞中的上一次出現位置

舉例來講,若是字符串"ABCDAB"的後一個"AB"是"好後綴"。那麼它的位置是5(從0開始計算,取最後的"B"的值),在"搜索詞中的上一次出現位置"是1(第一個"B"的位置),因此後移 5 - 1 = 4位,前一個"AB"移到後一個"AB"的位置。

再舉一個例子,若是字符串"ABCDEF"的"EF"是好後綴,則"EF"的位置是5 ,上一次出現的位置是 -1(即未出現),因此後移 5 - (-1) = 6位,即整個字符串移到"F"的後一位。

這個規則有三個注意點:

  • (1)"好後綴"的位置以最後一個字符爲準。假定"ABCDEF"的"EF"是好後綴,則它的位置以"F"爲準,即5(從0開始計算)。
  • (2)若是"好後綴"在搜索詞中只出現一次,則它的上一次出現位置爲 -1。好比,"EF"在"ABCDEF"之中只出現一次,則它的上一次出現位置爲-1(即未出現)。
  • (3)若是"好後綴"有多個,則除了最長的那個"好後綴",其餘"好後綴"的上一次出現位置必須在頭部。好比,假定"BABCDAB"的"好後綴"是"DAB"、"AB"、"B",請問這時"好後綴"的上一次出現位置是什麼?回答是,此時採用的好後綴是"B",它的上一次出現位置是頭部,即第0位。這個規則也能夠這樣表達:若是最長的那個"好後綴"只出現一次,則能夠把搜索詞改寫成以下形式進行位置計算"(DA)BABCDAB",即虛擬加入最前面的"DA"。

回到上文的這個例子。此時,全部的"好後綴"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"還出如今頭部,因此後移 6 - 0 = 6位。
image

12.能夠看到,"壞字符規則"只能移3位,"好後綴規則"能夠移6位。因此,Boyer-Moore算法的基本思想是,每次後移這兩個規則之中的較大值。

更巧妙的是,這兩個規則的移動位數,只與搜索詞有關,與原字符串無關。所以,能夠預先計算生成《壞字符規則表》和《好後綴規則表》。使用時,只要查表比較一下就能夠了。

image

  1. 繼續從尾部開始比較,"P"與"E"不匹配,所以"P"是"壞字符"。根據"壞字符規則",後移 6 - 4 = 2位。

image

  1. 從尾部開始逐位比較,發現所有匹配,因而搜索結束。若是還要繼續查找(即找出所有匹配),則根據"好後綴規則",後移 6 - 0 = 6位,即頭部的"E"移到尾部的"E"的位置。

image

算法四: Sunday算法

Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很類似:1

只不過Sunday算法是從前日後匹配,在匹配失敗時關注的是主串中參加匹配的最末位字符的下一位字符。

若是該字符沒有在模式串中出現則直接跳過,即移動位數 = 模式串長度 + 1;
不然,其移動位數 = 模式串長度 - 該字符最右出現的位置(以0開始) = 模式串中該字符最右出現的位置到尾部的距離 + 1。
下面舉個例子說明下Sunday算法。假定如今要在主串」substring searching」中查找模式串」search」。

剛開始時,把模式串與文主串左邊對齊:
image

結果發如今第2個字符處發現不匹配,不匹配時關注主串中參加匹配的最末位字符的下一位字符,即標粗的字符 i,由於模式串search中並不存在i,因此模式串直接跳過一大片,向右移動位數 = 匹配串長度 + 1 = 6 + 1 = 7,從 i 以後的那個字符(即字符n)開始下一步的匹配,以下圖:
image

結果第一個字符就不匹配,再看主串中參加匹配的最末位字符的下一位字符,是’r’,它出如今模式串中的倒數第3位,因而把模式串向右移動3位(m - 3 = 6 - 3 = r 到模式串末尾的距離 + 1 = 2 + 1 =3),使兩個’r’對齊,以下:
image

匹配成功。

回顧整個過程,咱們只移動了兩次模式串就找到了匹配位置,緣於Sunday算法每一步的移動量都比較大,效率很高。

算法五: BF算法(Brute Force)

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)。下面結合圖片,解釋一下:

Brute-Force

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算法

Aho-Corasick算法又叫AC自動機算法,是一種多模式匹配算法。Aho-Corasick算法能夠在目標串查找多個模式串,出現次數以及出現的位置。

Aho-Corasick算法主要是應用有限自動機的狀態轉移來模擬字符的比較,下面對有限狀態機作幾點說明
image
上圖是由多模式串{he,she,his,hers}構成的一個有限狀態機:

1.該狀態當字符匹配是按實線標註的狀態進行轉換,當全部實線路徑都不知足(即下一個字符都不匹配時)按虛線狀態進行轉換。

2.對ushers匹配過程以下圖所示:
image

當轉移到紅色結點時表示已經匹配而且得到模式串

Aho-Corasick算法步驟

Aho-Corasick算法和前面的算法同樣都要對模式串進行預處理,預處理主要包括字典樹Tire的構造,構建狀態轉移表(goto),失效函數(failure function),輸出表(Output)。

Aho-Corasick算法包括如下3個步驟

  1. 構建字典樹Tire
  2. 構建狀態轉移表,失效函數(failure function),輸出表(Output)
  3. 搜索路徑(進行匹配)

下面3個步驟分別進行介紹

  1. 構建字典樹Tire

Tire是哈希樹的變種,Tire樹的邊是模式串的字符,結點就是Tire的狀態表,下圖是多模式串{he,she,his,hers}的Tire樹結構:
image

  1. 構建goto函數、failure function和Output函數
  • goto函數(狀態轉移函數):goto(pre,v)=next,完成這樣的任務:在當前狀態pre,輸入字符v,獲得下一個狀態next,若是沒有下個狀態則next=failure。
  • failure function:失效函數是處理當前狀態是failure時的處理。
  • output函數:當完成匹配是根據狀態輸出匹配的模式串。

下面是多模式串{he,she,his,hers}的goto函數,failure函數,output函數

函數 結構圖
goto函數 image
failure函數 image
output函數 image
  1. 多模式串{he,she,his,hers}最終的有限狀態機圖

    image

特色

通常而言,好的字符串匹配算法要有如下特色:

速度快

這是評價一個字符匹配算法最重要的標準。一般要求字符匹配能以線性速度執行。

  • 有幾種時間複雜性的評價指標

序號 指標 描述
1) 預處理時間的複雜性 有些算法在進行字符串匹配前須要對模式特徵進行預處理
2) 匹配階段的時間複雜性 字符串匹配過程當中執行查找操做的時間複雜性,它一般和文本長度及模式長度相關
3) 最壞狀況下的時間複雜性 對一個text進行字符模式匹配時,設法下降各算法的最壞狀況下的時間複雜性是目前的研究熱點之一
4) 最好狀況下的時間複雜性 對一個text進行字符模式匹配時的最好的可能性。

內存佔用少

執行預處理和模式匹配不只須要CPU資源還須要內存資源,儘管目前內存的容量較之前大得多,但爲了提升速度,人們常利用特殊硬件。一般,特殊硬件中內存訪問速度很快但容量偏小,這時,佔用資源少的算法將更具優點。

參考

相關文章
相關標籤/搜索