字符串匹配--Sunday算法

字符串匹配(查找)算法是一類重要的字符串算法(String Algorithm)。有兩個字符串, 長度爲m的haystack(查找串)和長度爲n的needle(模式串), 它們構造自同一個有限的字母表(Alphabet)。若是在haystack中存在一個與needle相等的子串,返回子串的起始下標,不然返回-1。C/C++、PHP中的strstr函數實現的就是這一功能。LeetCode上也有相似的題目,好比#28#187.java

這個問題已經被研究了n多年,出現了不少高效的算法,比較著名的有,Knuth-Morris-Pratt 算法 (KMP)、Boyer-Moore搜索算法、Rabin-Karp算法、Sunday算法等。
提出Sunday算法的人叫Sunday,怎麼就不能起些狂拽酷炫吊炸天的名字好比hurricane algorithm/bazinga algorithm 之類的呢?-_-||算法

針對這個問題,Brut-force的解法很直觀:兩個串左端對其,而後從needle的最左邊字符往右逐一匹配,若是出現失配,則將needle往右移動一位,繼續從needle左端開始匹配...如此,直到找到一串完整的匹配,或者haystack結束。時間複雜度是O(mn),看起來不算太糟。入下圖所示:
圖中紅色標記的字母表示第一個發生失配的位置,綠色標記的是完整匹配的位置。


重複這個匹配、右移的過程,每次只將needle右移一個位置

直到找到這麼個完整匹配的子串。
數組

限制這個算法效率的因素在於,有不少重複的沒必要要的匹配嘗試。所以想辦法減小沒必要要的匹配,就能提升效率咯。不少高效的字符串匹配算法,它們的核心思想都是同樣樣的,想辦法利用部分匹配的信息,減小沒必要要的嘗試。
Sunday算法利用的是發生失配時查找串中的下一個位置的字母。仍是用圖來講明:

上圖的查找中,在haystack[1]和needle[1]的位置發生失配,接下來要作的事情,就是把needle右移。在右移以前咱們先把注意力haystack[3]=d這個位置上。若是needle右移一位,needle[2]=c跟haystack[3]對應,若是右移兩位,needle[1]=b跟haystack[3]對應,若是移三位,needle[0]=a跟haystack[3]對應。而後不管以上狀況中的哪種,在haystack[3]這個位置上都會失配(固然在這個位置前面也可能失配),由於haystack[3]=d這個字母根本就不存在於needle中。所以更明智的作法應該是直接移四位,變成這樣:

而後咱們發如今needle[0]=a,haystack[4]=b位置又失配了,因而沿用上一步的思路,看看haystack[7]=b。此次咱們發現字母b是在needle中存在的,那它就有可能造成一個完整的匹配,由於咱們徹底直接跳過,而應該跳到haystack[7]與needle[1]對應的位置,以下圖:

這一次,咱們差點就找到了一個完整匹配,惋惜needle[0]的位置失配了。不要氣餒,再日後,看haystack[9]=z的位置,它不存在於needle中,因而跳到z的下一個位置,而後...:

因而咱們順利地找到了一個匹配!
而後試着從上面的過程當中總結出一個算法來。安全

輸入: haystack, needle
Init: i=0, j=0
while i<=len(haystack)-len(needle):
    j=0
    while j<len(needle) and haystack[i+j] equals needle[j]:
        j=j+1
    if j equals len(needle):
        return i
    else
        increase i...

這裏有一個問題,發生失配時,i應該增長多少。若是haystack[i+j]位置的字母不存在於needle中,咱們知道能夠跳到i+j+1的位置。而若是chr=haystack[i+j]存在於needle,咱們說能夠跳到使chr對應needle中的同一個字母的位置。但問題是,needle中可能有不止一個的字母等於chr。這種狀況下,應該跳到哪個位置呢?爲了避免遺漏可能的匹配,應該是跳到使得needle中最右一個chr與haystack[i+j]對應,這樣跳過的距離最小,且是安全的。
因而咱們知道,在開始查找以前,應該作一項準備工做,收集Alphabet中的字母在needle中最右一次出現的位置。咱們創建一個O(k)這麼大的數組,k是Alphabet的大小,這個數組記錄了每個字母在needle中最右出現的位置。遍歷needle,更新對應字母的位置,若是一個字母出現了兩次,前一個位置就會被後一個覆蓋,另外咱們用-1表示根本不在needle中出現。
用occ表示這個位置數組,求occ的過程以下:函數

輸入: needle
Init: occ is a integer array whose size equals len(needle)
fill occ with -1
i=0
while i<len(needle):
    occ[needle[i]]=i
return occ

還有一點須要注意的是,Sunday算法並不限制對needle串的匹配順序,能夠從左往右掃描needle,能夠從右往左,甚至任何自定義的順序。
接下來嘗試具體實現一下這個算法,如下是Java程序,這裏假設Alphabet就是ASCII字符集。優化

public int strStr(String haystack, String needle) {
        int m=haystack.length(), n=needle.length();
        int[] occ=getOCC(needle);
        int jump=0;
        for(int i=0;i<=m-n; i+=jump){
            int j=0;
            while(j<n&&haystack.charAt(i+j)==needle.charAt(j))
                j++;
            if(j==n)
                return i;
            jump=i+n<m ? n-occ[haystack.charAt(i+n)] : 1;
        }
        return -1;
    }

    public int[] getOCC(String p){
        int[] occ=new int[128];
        for(int i=0;i<occ.length;i++)
            occ[i]=-1;
        for(int i=0;i<p.length();i++)
            occ[p.charAt(i)]=i;
        return occ;
    }

如今來分析一下算法。除去預處理階段計算occ數組,Sunday算法的主要操做是匹配字符和移動(改變haystack的遊標i)。算法的時間複雜度主要依賴兩個因素,一是i每次能跳過的位置有多少;二是在內部循環嘗試匹配時,多快能肯定是失配了仍是完整匹配了。在最好的狀況下,每次失配,occ[haystack[i+j]]都是-1,因而每次i都跳過n+1個位置;而且當在內部循環嘗試匹配,總能在第一個字符位置就肯定失配了,這樣獲得時間O(m/n)。好比下圖這種狀況:

最壞狀況下,每次i都只能移動一位,且老是幾乎要到needle的末尾才發現失配了。時間複雜度是O(m*n)並不比Brut-force的解法好。好比像這樣:

前面提到Sunday算法對needle的掃描順序是沒有限制的。爲了提升在最壞狀況下的算法效率,能夠對needle中的字符按照其出現的機率從小到大的順序掃描,這樣能儘早地肯定失配與否。
Sunday算法其實是對Boyer-Moore算法的優化,而且它更簡單易實現。其論文中提出了三種不一樣的算法策略,結果都優於Boyer-Moore算法。spa

Reference:
1] [D.M. Sunday: A Very Fast Substring Search Algorithm. Communications of the ACM, 33, 8, 132-142 (1990)
2] [Fachhochschule Flensburgcode

本文遵照知識共享協議:署名-非商業性使用-相同方式共享 (BY-NC-SA)簡書協議轉載請註明:做者曾會玩htm

相關文章
相關標籤/搜索