子字符串查找算法----暴力算法、KMP算法、Boyer-Moore算法和Rabin-Karp算法

各類算法比較

優缺點:

優勢:java

  • 暴力查找算法:實現簡單且在通常狀況下工做良好(Java的String類型的indexOf()方法就是採用暴力子字符串查找算法);
  • Knuth-Morris-Pratt算法可以保證線性級別的性能且不須要在正文中回退;
  • Boyer-Moore算法的性能通常狀況下都是亞線性級別;
  • Rabin-Karp算法是線性級別;

缺點:c++

  • 暴力查找算法所需時間可能和NM成正比;
  • Knuth-Morris-Pratt算法和Boyer-Moore算法須要額外的內存空間;
  • Rabin-Karp算法內循環很長(若干次算術運算,其餘算法都只須要比較字符);

成本總結:

算法 版本 最壞狀況 通常狀況 是否回退 正確性 額外空間需求
暴力算法 -- MN 1.1N 1
KMP算法 完整的DFA 2N 1.1N MR
Boyer-Moore算法 啓發式查找不匹配字符 MN N/M R
Rabin-Karp算法 蒙特卡洛算法 7N 7N 是* 1
  拉斯維加斯算法 7N* 7N 1

1  暴力查找法

設文本長度爲N,要匹配的模式的長度爲M,暴力查找算法在最壞的狀況下運行時間與MN成正比,但在處理許多應用程序中的字符串時,它的實際運行時間通常與M+N成正比。算法

算法實現1:

使用一個值指針i跟蹤文本,一個指針j跟蹤要匹配的模式,對每個i,代碼首先將j重置爲0並不斷增大,直到找到了一個不匹配的字符或者是匹配成功(j==M)。數組

public static int search(String pat, String txt) {
	int M = pat.length();
	int N = txt.length();
	for(int i = 0;i<= N;i++) {
		int j;
		for(j=0;j<M;j++)
			if(txt.charAt(i+j)!=pat.charAt(j))
				break;
		if(j==M)	return i;
	}
	return N;
}

算法實現2(顯式回退):

一樣使用一個值指針i跟蹤文本,一個指針j跟蹤要匹配的模式,在i和j指向的字符匹配時,i和j同時後移一位。若是i和j字符不匹配,那麼須要回退這兩個指針,j指向模式的開頭,i指向此次匹配開頭的下一個字符。dom

public static int Search(String pat,String txt) {
	int j, M = pat.length();
	int i, N = txt.length();
	for(i=0,j=0;i<N&&j<M;i++) {
		if(txt.charAt(i)==pat.charAt(j))	j++;
		else {i-=j;j=0;}
	}
	if(j==M)	return i-M;
	else	return N;
}

2  KMP算法

算法思想:

當出現不匹配時,就能知曉一部份內容(由於匹配失敗以前的字符已經和模式相匹配)。能夠利用這些信息避免指針回退。使人驚訝的是,KMP算法在匹配失敗時,總能將j設置爲一個值以使i不回退。函數

在KMP算法中,不會回退文本指針i,而是用一個數組dfa[][]來記錄匹配失敗時指針j應該回退多遠。對於每個字符c,在比較了c和pat.charAt(j)後,dfa[c][j]表示的是應該和下一個文本字符比較的模式字符的位置。在匹配時會繼續比較下一個字符,所以dfa[pat.charAt(j)][i]老是j+1; 在不匹配時,不只能夠知道txt.charAt(i)的字符,還能夠知道正文中前j-1個字符--它們就是模式中的前j-1個字符。性能

代碼實現:

public class KMP {
    private final int R;     
    private int[][] dfa;          
    private String pat;        
    //構造函數中生成dfa[][]數組
    public KMP(String pat) {
        this.R = 256;
        this.pat = pat;
        int m = pat.length();
        dfa = new int[R][m]; 
        dfa[pat.charAt(0)][0] = 1; 
        for (int x = 0, j = 1; j < m; j++) {
            for (int c = 0; c < R; c++) 
                dfa[c][j] = dfa[c][x];    
            dfa[pat.charAt(j)][j] = j+1;    
            x = dfa[pat.charAt(j)][x];     
        } 
    } 
    //search()方法經過dfa[][]中計算的值來查找子字符串
    public int search(String txt) {
        int m = pat.length();
        int n = txt.length();
        int i, j;
        for (i = 0, j = 0; i < n && j < m; i++) {
            j = dfa[txt.charAt(i)][j];
        }
        if (j == m) return i - m; 
        return n;                   
    }
}

對於長度爲M的模式字符串和長度爲N的文本,KMP子字符串查找算法訪問的字符串不會超過M+N個。this

3  Boyer-Moore算法

Boyer-Moore算法是一種從右向左掃描模式字符串並將它與文本匹配的算法。spa

算法思想:

有文本FINDINAHAYSTACKNEEDLE和模式字符串NEEDLE. 由於是從右向左掃描,因此會先比較模式中最後一位E和文本中下標爲5的N。不匹配,由於模式字符串中也出現了N,則右移模式字符串使得模式中最右邊的N(這裏是位置0的N)與文本中的相應N對齊。而後接着比較模式字符串最後的E和文本中的S(下標10),不匹配,並且模式中不含有字符S,能夠將模式直接右移6位,而後繼續匹配......指針

上述方法被稱爲啓發式的處理不匹配字符。要實現之,須要一個數組right[]保存字母表中每一個字母在模式字符串中出現的最靠右的下標(若是不存在則爲-1)。這個值揭示了若是發生不匹配,應該右跳躍多遠。

在right[]數組計算後,算法實現起來就很是容易了。用一個索引i在文本中從左向右移動,用索引j在模式字符串中從右向左移動。內循環檢查檢查正文和模式字符串在位置i是否相等,若是從M-1到0的全部j,txt.charAt(i+j)都和pat.charAt(j)相等,就是找到了匹配。不然匹配失敗,失敗有三種狀況:

  1. 若是形成失敗的字符不包含在模式字符串中,則將模式字符串向右移動j+1個位置;
  2. 若是形成失敗的字符包含在模式字符串中,根據right[]數組右移模式字符串;
  3. 若是這種方法沒法增大i,就直接將i+1保證模式字符串至少向右移動一個位置。

在通常狀況下,對於長度爲N的文本和長度爲M的模式字符串,該方法經過啓發式處理不匹配的字符須要~N/M次比較。

算法實現:

public class BoyerMoore {
    private final int R; 
    private int[] right;   
    private String pat;     

    public BoyerMoore(String pat) {
        this.R = 256;
        this.pat = pat;

        right = new int[R];
        for (int c = 0; c < R; c++)
            right[c] = -1;
        for (int j = 0; j < pat.length(); j++)
            right[pat.charAt(j)] = j;
    }
    
    public int search(String txt) {
        int m = pat.length();
        int n = txt.length();
        int skip;
        for (int i = 0; i <= n - m; i += skip) {
            skip = 0;
            for (int j = m-1; j >= 0; j--) {
                if (pat.charAt(j) != txt.charAt(i+j)) {
                    skip = Math.max(1, j - right[txt.charAt(i+j)]);
                    break;
                }
            }
            if (skip == 0) return i;
        }
        return n;              
    }
}

4  Rabin-Karp算法

Rabin-Karp算法是一種基於散列的子字符串查找算法--先計算模式字符串的散列值,而後用相同的散列函數計算文本中全部可能的M個字符的子字符串的山裂紙並與模式字符串的散列值比較。若是二者相同,再繼續驗證二者是否匹配。

算法思想:

長度爲M的對應着一個R進制的M位數,

舉例說明Rabin-Karp算法:例如要在文本3141592653589793中找到模式26535,首先選擇散列表大小Q(這裏設置爲997),採用除留餘數法,散列值爲26535%997 = 613,而後計算文本中全部長度爲5的字符串的散列值並尋找匹配。

算法實現:

實現Rabin-Karp算法關鍵是要找到一種方法可以快速地計算出文本中全部長度等於要匹配字符串長度的子字符串的散列值。也就是對全部位置i,  高效計算出文本中i+1位置的子字符串的值。具體算法爲:假設已知h(xi) = xi mod Q, 將模式字符串右移一位等價於將xi替換爲x(i+1), x(i+1)等於xi減去第一個數字的值,乘以R,再加上最後一個數字的值。這麼作的結果是不管M是五、100仍是1000,均可以在常數時間內不斷地一格一格向後移動。

計算散列函數:

對於5位的數,能夠用int直接計算,但若是M等於100、1000就不行了。這時候可使用Horner方法。其計算方法以下:

private long hash(String key, int m) {   
    long h = 0; 
    for (int j = 0; j < m; j++) 
        h = (R * h + key.charAt(j)) % q;
    return h;
}

查找實現:

有兩種表明實現:蒙特卡洛方法和拉斯維加斯方法。

  • 蒙特卡洛方法是選取很大的Q值,使得散列衝突極小,這樣能夠保證散列值相同就是匹配成功;
  • 拉斯維加斯方法則是散列值相同後再去比較字符,效率不如上一種方法,但能夠保證正確性。
public class RabinKarp {
    private String pat;    
    private long patHash;   
    private int m;       
    private long q;       
    private int R;         
    private long RM;      

    public RabinKarp(String pat) {
        this.pat = pat;
        R = 256;
        m = pat.length();
        q = longRandomPrime();

        RM = 1;
        for (int i = 1; i <= m-1; i++)
            RM = (R * RM) % q;
        patHash = hash(pat, m);
    } 

    private long hash(String key, int m) { 
        long h = 0; 
        for (int j = 0; j < m; j++) 
            h = (R * h + key.charAt(j)) % q;
        return h;
    }

    private boolean check(String txt, int i) {
        for (int j = 0; j < m; j++) 
            if (pat.charAt(j) != txt.charAt(i + j)) 
                return false; 
        return true;
    }

    public int search(String txt) {
        int n = txt.length(); 
        if (n < m) return n;
        long txtHash = hash(txt, m); 

        if ((patHash == txtHash) && check(txt, 0))
            return 0;

        for (int i = m; i < n; i++) {
            txtHash = (txtHash + q - RM*txt.charAt(i-m) % q) % q; 
            txtHash = (txtHash*R + txt.charAt(i)) % q; 

            int offset = i - m + 1;
            if ((patHash == txtHash) && check(txt, offset))
                return offset;
        }
        return n;
    }
    
    private static long longRandomPrime() {
        BigInteger prime = BigInteger.probablePrime(31, new Random());
        return prime.longValue();
    }
}
相關文章
相關標籤/搜索