Rabin-Karp指紋字符串查找算法


 

首先計算模式字符串的散列函數, 如果找到一個和模式字符串散列值相同的子字符串, 那么繼續驗證兩者是否匹配.

這個過程等價於將模式保存在一個散列表中, 然后在文本中的所有子字符串查找. 但不需要為散列表預留任何空間, 因為它只有一個元素.

 

基本思想

長度為M的字符串對應着一個R進制的M位數, 為了用一張大小為Q的散列表來保存這種類型的鍵, 需要一個能夠將R進制的M位數轉化為一個0到Q-1之間的int值散列函數, 這里可以用除留取余法.

舉個例子, 需要在文本 3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3 查找模式 2 6 5 3 5, 這里R=10, 取Q=997, 則散列值為

2 6 5 3 6 % 997 = 613

然后計算文本中所有長度為5的子字符串並尋找匹配

3 1 4 1 5 % 997 = 508

1 4 1 5 9 % 997 = 201

......

2 6 5 3 6 % 997 = 613 (匹配)

 

計算散列函數

對於5位的數值, 只需要使用int就可以完成所有需要的計算, 但是當模式長度太大時, 我們使用Horner方法計算模式字符串的散列值

2 % 997 = 2

2 6 % 997 = (2*10 + 6) % 997 = 26

2 6 5 % 997 = (26*10 + 5) % 997 = 265

2 6 5 3 % 997 = (265*10 + 3) % 997 = 659

2 6 5 3 5 % 997 = (659*10 + 5) % 997 = 613

 

這里關鍵的一點就是在於不需要保存這些數的值, 只需保存它們除以Q之后的余數.

取余操作的一個基本性質是如果每次算術操作之后都將結果除以Q並取余, 這等價於在完成所有算術操作之后再將最后的結果除以Q並取余.

 

算法實現

 

3 1 4 1 5 9 2 6 5 3 5 8 9 7 9 3

3 % 997 = 3

3 1 % 997 = (3*10 + 1) %997 = 31

3 1 4 % 997 = (31*10 + 4) % 997 = 314

3 1 4 1 % 997 = (314*10 + 1) % 997 = 150

3 1 4 1 5 % 997 = (150*10 + 5) % 997 = 508

   1 4 1 5 9 % 997 = ( (508 + 3*(997 - 30) ) *10 + 9) % 997 = 201

      4 1 5 9 2 % 997 = ( (201 + 1*(997 - 30) ) *10 + 2) % 997 = 715

    ......

             2 6 5 3 6 % 997 =  ( (929 + 9*(997 - 30) ) *10 + 5) % 997 = 613

 

 

構造函數為模式字符串計算了散列值patHash並在變量中保存了R^(M-1) mod Q的值, hashSearch()計算了文本前M個字母的散列值並和模式字符串的散列值比較, 如果沒有匹配, 文本指針繼續下移一位, 計算新的散列值再次比較,知道成功或結束.

 

import java.math.BigInteger;
import java.util.Random;

import edu.princeton.cs.algs4.StdOut;

public class RabinKarp {
    private String pat;    //模式字符串
    private long patHash;    //模式字符串散列值
    private int M;     //模式字符串的長度
    private long Q;    //很大的素數
    private int R;    //字母表的大小
    private long RM;    //R^(M-1) % Q

    public RabinKarp(char[] pat, int R){
        this.pat = String.valueOf(pat);
        this.R = R;
    }
    
    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 str, int M){
        long h = 0;
        for(int i=0; i < M; i++){
            h = (R * h + str.charAt(i)) % Q;
        }
        return h;
    }
    
    public 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;
    }
    
    private static long longRandomPrime() {
        BigInteger prime = BigInteger.probablePrime(31, new Random());
        return prime.longValue();
    }
    
    private int search(String txt){
        int N = txt.length();
        if(N < M) return N;
        long txtHash = hash(txt,M);
        
        if((txtHash == patHash) && 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;
    }
    
    public static void main(String[] args) {
        String pat = args[0];
        String txt = args[1];

        RabinKarp searcher = new RabinKarp(pat);
        int offset = searcher.search(txt);
        // print results
        StdOut.println("text:    " + txt);

        // from brute force search method 1
        StdOut.print("pattern: ");
        for (int i = 0; i < offset; i++)
            StdOut.print(" ");
        StdOut.println(pat);
    }
}
View Code

 

 

上面代碼中的求模運算的方法可以參考初數論里面的同模定理.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM