算法——字符串匹配之Rabin-Karp

有时间的读者请先看它 Rabin-Karp——geeksforgeeks

主字符串用S代替,长度为N;模式字符串用P代替,长度为M。

一种类似指纹的搜索想法:如果我们可以在O(M)的时间里计算P的指纹f(P),如果f(P) ≠ f(S[s…s+M-1])那么P ≠ S[s…s+M-1],如果我们可以在O(1)的时间里比较指纹,如果我们可以在O(1)的时间里从f(T[s…s+M-1])计算f(S[s+1…s+M]),那么字符串比较的速度将会加快。

举例:假设主字符串中只有数字0-9,我们搜索一个子串”1045”的位置。
1. 字母表: ∑ = {0,1,2,3,4,5,6,7,8,9},字母表大小为10;
2. 指纹计算:f(“1045”) = 1*10^3+0*10^2+4*10^1+5*10^0 = 1045
3. 算法伪代码描述:

Fingerprint_Search(S, P)
01: fp <- compute f(P)          // 预处理
02: f <- compute f(S[0...M-1])  // 预处理
03: for i <- 0 to N-M Do        // 开始搜索
04:     if fp = f return i
05:     f = (f-S[i]*10^(m-1))*10+S[i+M]
06: return -1

以上的伪代码对搜索0-9数字看起来成立,而且运行时间为2O(M)+O(N-M)=O(N+M)很好。可是我们如何搜索其他字符呢,我们如何能在非常快的时间里计算出指纹呢?不过以上的如果和问题都可以得到解决,接下来介绍Rabin-Karp算法。

Rabin-Karp

  1. Rabin-Karp字符串匹配算法和之前介绍的朴素匹配算法类似,也是对每一个字符进行比较。不同的是Rabin-Karp采用了把字符进行预处理,通过某种函数计算其函数值(指纹),比较的是每个字符的函数值。

  2. 算法分析:预处理时间O(M),匹配时间是O(N-M)(最好)O((N-M+1)M)(最坏)。

  3. 计算指纹的函数如何选取?采用Hash函数,简单来说就是取模运算:h = f mod q(q为素数,将函数值限制在q之内)
    (1) 如果q=7,那么h(“52”) = f mod q = 52 mod 7 = 3;
    (2) 如果h(s1) ≠ h(s2),那么s1 ≠ s2;
    (3) 如果h(s1) = h(s2),不代表s1 = s2;(q = 7 时,h(“52”)=h(“94”),但”52”≠”94”,这种情况下我们需要按位比较每一个字符
    (4) (a+b) mod q = (a mod q + b mod q) mod q;
    (5) (a*b) mod q = (a mod q * b mod q )mod q

  4. Rabin-Karp字符串匹配如何扩展到字母甚至所有字符?比较数字时,我们定义字母表为{0,1,2,3,4,5,6,7,8,9},字母表大小为10,使用十进制。若比较字母的话,我们就可以定义字母表为{a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z},使用26进制。那么我们计算hash函数值的时候就可以变化为f(“abcd”) = 0*26^3 + 1*26^2 + 2 * 26^1 + 3 * 26^0 = 731(该例子a,b,c,d看作1,2,3,4,方便理解。实际代码中我们用的是ascii码,因为char字符在进行加减乘除运算中会自动转化为int值)。若是比较数字加字母,定义字母表为二者的并集,进制变为36。

  5. 算法中的几个细节(仍然按照十进制来讲解)
    预处理中
    (1) fp = (p[m-1] + 10*(p[m-2]+ 10*(p[m-3]+…+10*(p[1]+10*p[0]))))modq(秦九韶算法,将一元n次多项式的求值问题转化为n个一次式的算法,简化计算过程,在西方被称作霍纳[Horner]算法);
    (2) f(S[0…m-1]) 计算方法 同(1);
    循环比较中
    (3) f(S[i+1…i+M]) = (f(S[i…i+M-1])-S[i]* 10M1 )*10 + S[i + M]) mod q;
    (4) 其中 10M1 可以在预处理中计算出来结果。

  6. 算法优化后的伪代码描述

Rabin_Karp_Search(S, P)
01: q <- a prime number larger than M(Plength)
02: c <- 10^(M-1)mod q
03: fp <- 0; fs <- 0;
04: for i <- 0 to M-1
05:     fp <- (10 * fp + P[i]) mod q
06:     fs <- (10 * fs + S[i]) mod q
07: for i <- 0 to N-M
08:     if fp = ft then // run a loop to compare string(hash函数值相等则匹配字符串)
09:         if P[0...M-1] = T[i...i+M-1] return i
10:     fs = ((fs - S[i]*c)+S[i+M]) mod q
11: return -1
  1. Rabin-Karp再分析
    (1) q 为素数, hash函数会使M位字符串的hash函数值在q个值内均匀分布(q越大越好)。所以,比较字符串外层循环从i = 0 —> N-M中,每q次才需要出现hash函数值相等的情况,此情况下需要比较字符串。因为出现的概率低,所以算法会比较快。
    (2) 选择位数 > M 的素数 q,可以利用随机算法在O(M)完成。
    (3) 预处理时间O(M),外层循环为O(N-M),所以所有内循环之和为(N-M)* M/q = O(N-M),所以运行时间为O(N-M)。
    (4) 最坏运行时间O(NM),例如S=”aaaaaaaaaaaaab” P=”aaab”

  2. Rabin-Karp代码(Java版)

public class RabinKarpSearch {
    // d is the number of characters in input alphabet
    public final static int d = 256;

    public static void search( String S, String P, int q) {
        int N = S.length();
        int M = P.length();
        int t = 0; // hash value for S:txt
        int p = 0; // hash value for P:pattern
        int h = 1;
        int i, j;

        // The value of h would be "pow(d, M-1)%q"
        for (i = 0; i < M - 1; i++)
            h = (h * d) % q;

        // Calculate the hash value of pattern and S[0...M-1]
        for (i = 0; i < M; i++) {
            p = (d * p + P.charAt(i)) % q;
            t = (d * t + S.charAt(i)) % q;
        }

        // Slide the pattern over text one by one
        for (i = 0; i <= N - M; i++) {
            // Check the hash values of current window of text and pattern. 
            // If the hash values match then check for characters on by one
            if (p == t) {
                for (j = 0; j < M; j++) {
                    if (S.charAt(i + j) != P.charAt(j))
                        break;
                }

                // if p == t and pat[0...M-1] = txt[i, i+1, ...i+M-1]
                if (j == M)
                    System.out.println("Pattern found at index " + i);
            }

            // Calculate hash value for next window of text: Remove
            // leading digit, add trailing digit
            if (i < N - M) {
                t = ( (t - S.charAt(i) * h) * d + S.charAt(i + M)) % q;
                // We might get negative value of t, converting it to positive
                if (t < 0)
                    t = (t + q);
            }
        }
    }

    public static void main(String[] args) {
        String S = "GEEKS FOR GEEKS";
        String P = "GEEK";
        int q = 101; // A prime number
        search(S, P, q);
    }
}

参考资料:
[1]. Rabin-Karp——geeksforgeeks
[2]. 算法——字符串匹配之Rabin-Karp算法
[3]. 面试算法之字符串匹配算法,Rabin-Karp算法详解

你可能感兴趣的:(默认,算法)