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

来源:互联网 发布:pdf软件下载 编辑:程序博客网 时间:2024/05/29 10:58

有时间的读者请先看它 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 i05:     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 q03: fp <- 0; fs <- 0;04: for i <- 0 to M-105:     fp <- (10 * fp + P[i]) mod q06:     fs <- (10 * fs + S[i]) mod q07: for i <- 0 to N-M08:     if fp = ft then // run a loop to compare string(hash函数值相等则匹配字符串)09:         if P[0...M-1] = T[i...i+M-1] return i10:     fs = ((fs - S[i]*c)+S[i+M]) mod q11: 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算法详解

原创粉丝点击