代码改变世界

字符串匹配算法Rabin-Karp 算法的学习笔记

2011-04-16 22:11  Aga.J  阅读(987)  评论(0编辑  收藏  举报

  该算法的思想是,通过对模式字符串进行hash运算,同时对源字符串取长度跟模式字符串相等的子字符串也进行hash运算,最后比较hash值来确定模式字符串是否和源字符串的子串匹配,并获得其匹配起始位置。

   什么叫做hash运算呢?把串看作是字符集(这个串中的字符是属于某个字符集的)长度进制的数(比如数字串'’123”的字符集就是0到9,属于10进制,将数字串转换为相应的10进制数进行比较),由“数”的比较得出字符串的比较结果。例如,给定字符集为∑ ={0,1,2,3,4,5,6,7,8,9} ,∑长度为 d=10 ,那么任何以∑为字符集的串都可看作 d (此处为 10 )进制的数。

  设模式串 P[0..m-1] 对应的数值为 P (例如串“123”对应的数值就是“一百二十三”),所有在源串中T中长度为 m 的子串T[0..m-1]对应的数值为 t0 ,ts 即为 T[s..s+m] 对应的数值,这里 0<=s<=n-m-1 。设 P 和 T 都是基于字符集长度为 | ∑ |=d 的字符串。

  下面介绍如何将串转化为对应的数值:

  首先对模式字符串,利用Horner法则p=p[m]+10(p[m-1]+10(p[m-2]+..+10(p[2]+10p[1])…)),得到hash值p, 然后再对源字符串,对应每个长度为m的子串,求hash值

t(s+1) =10(t(s) – 10^(m-1)T[s+1])+T[s+m+1],这里是从t(s)推出t(s+1)最后比较hash值是否相同,若相同则需要进一步按位比较,若不同,则字符串一定不同。

  为什么对源字符串不用p=p[m]+10(p[m-1]+10(p[m-2]+..+10(p[2]+10p[1])…))来求解呢,因为我们可以完全可以利用已经得到或者说已经计算过的信息来得到下一个长度为m的子串的数值。此处是该算法的关键,即在常数时间内能够计算出下一个 m 长度的字串对应的数值。初看比较抽象,举个例子就比较明白了,设 源串x=12345 ,现在是已知模式串的长度为 3,并且t(1)数值 234 ,现在要求计算t(2)的数值,就是345 对应的数值,可以这样来得到: 345 = 5 + 10*(234-102 *2) ----(看到了吗,这里用到t(1)已经求好的数值就可以退出t(2)了)

具体实现(从网上copy的,当然自己也需要实现一次以熟悉算法)

  1. #include "iostream"
  2. #include "string"
  3. #include "cmath"
  4. using namespace std;
  5. // get the value of the character in the set
  6. int getV(char p, string set)
  7. {
  8. for(int i=0; i<set.length(); i++)
  9. {
  10. if (p==set[i])
  11. return i;
  12. }
  13. return -1;
  14. }
  15. // d is the size of the character set
  16. int RK(string T, string P,string set)
  17. {
  18. int d = int(set.length());            //集合的长度是进制数
  19. int n = T.length();
  20. int m = P.length();
  21. int h = pow(double(d), m-1);
  22. int p = 0;
  23. int t = 0;
  24. for(int i=0; i<m; i++)
  25. {
  26. p = d*p + getV(P[i],set);

                                                        //计算模式串的hash值,getV返回串在进制数d下的数值,求hash值

  1. t = d*t + getV(T[i], set);

                                                       // t(0)也是按照同样的办法求解

  1. }
  2. for (int s=0; s<=n-m; s++)

                                                        // ts 即为 T[s..s+m] 对应的数值,这里 0<=s<=n-m-1,ts+1 =T[s+m]+d*(ts +dm-1 *T[s])

  1. {
  2. cout<<"p,t is "<<p<<","<<t<<endl;
  3. if (p==t)                                       //刚好p等于t
  4. return s;
  5. if (s<n-m)
  6. t = getV(T[s+m],set)+d*(t-h*getV(T[s],set));

                                                 //在常数时间内得到t(s+1)的值,再和p做比较。

                                                  //看上文的例子

    }

  1. return -1;
  2. }
  3. int main()
  4. {
  5. // set is the character set
  6. string set= "0123456789"; //定义字符集
  7. // pattern P
  8. string P = "2365"; //模式串
  9. // T is the string to match
  10. string T = "258569236589780"; //源串
  11. int i = RK(T, P, set); //Rabin-Karp 算法,找到匹配位置
  12. cout<<"the postition is:"<<i<<endl;
  13. return 0;
  14. }

ps(个人观点):很明显的一点是这种算法和串的字符集空间大小有关,如果是中文呢?那怎么做?而如果仅仅是英文字符,我们可以利用asc||码值,将英文字符转换为对应的数值,加速了预处理(代码中的getV函数)的过程。