Rabin-Karp算法学习

Rabin-Karp 算法(字符串快速查找)

 

传统的字符串搜索算法时间复杂度为O(nm),其中n=|t|,m=|p|。具体代码如下:
int findmatch(char *p, char *t)
{
<span style="white-space:pre">	</span>int i, j; /*counters*/
<span style="white-space:pre">	</span>int m, n; /*string lengths*/

<span style="white-space:pre">	</span>m = strlen(p);
<span style="white-space:pre">	</span>n = strlen(t);

<span style="white-space:pre">	</span>for (i = 0; i <= (n - m); i++) {
<span style="white-space:pre">		</span>j = 0;
<span style="white-space:pre">		</span>while ((j < m) && (t[i + j] == p[j]))
<span style="white-space:pre">			</span>j = j + 1;
<span style="white-space:pre">		</span>if (j == m) return (i);
}

<span style="white-space:pre">	</span>return (-1);
}
Rabin-Karp 算法是基于这样的思路:即把字符串看作是字符集长度进制的数,由数值的比较结果得出字符串的比较结果。

  朴素的字符串匹配算法为什么慢?因为它太健忘了,前一次匹配的信息其实有部分可以应用到后一次匹配中去,而朴素的字符串匹配算法只是简单的把这个信息扔掉,从头再来,因此,浪费了时间。好好的利用这些信息,自然可以提高运行速度。

  由于完成两个字符串的比较需要对其中包含的字符进行逐个比较,所需的时间较长,而数值比较则一次就可以完成,那么我们首先把“搜索词”中各个字符的“码点值”通过计算,得出一个数值(这个数值必须可以表示出字符的前后顺序,而且可以随时去掉某个字符的值,可以随时添加一个新字符的值),然后对“源串”中要比较的部分进行计算,也得出一个数值,对这两个数值进行比较,就能判断字符串是否匹配。对两个数值进行比较,速度比简单的字符串比较快很多。

  比如我们要在源串 "9876543210520" 中查找 "520",因为这些字符串中只有数字,所以我们可以使用字符集 {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} 来表示字符串中的所有元素,并且将各个字符映射到数字 09,然后用 M 表示字符集中字符的总个数,这里是 10,那么我们就可以将搜索词 "520" 转化为下面的数值:

("5"的映射值 * M + "2"的映射值) * M + "0"的映射值 = (5 * 10 + 2) * 10 + 0 = 520

  当然,如果“搜索词”很长,那么计算出来的这个数值就会很大,这时我们可以选一个较大的素数对其取模,用取模后的值作为“搜索词”的值。

  分析一下这个数值:520,它可以代表字符串 "520",其中:

代表字符 "5" 的部分是“ "5"的映射值 * (M 的 n - 1 次方) = 5 * (102 次方) = 500”
代表字符 "2" 的部分是“ "2"的映射值 * (M 的 n - 2 次方) = 2 * (101 次方) = 20”
代表字符 "0" 的部分是“ "0"的映射值 * (M 的 n - 3 次方) = 0 * (100 次方) = 0”
(n 代表字符串的长度)

  我们可以随时减去其中一个字符的值,也可以随时添加一个字符的值。

  “搜索词”计算好了,那么接下来计算“源串”,取“源串”的前 n 个字符(n 为“搜索词”的长度)"987",按照同样的方法计算其数值:

("9"的映射值 * M + "8"的映射值) * M + "7"的映射值 = (9 * 10 + 8) * 10 + 7 = 987

  然后将该值与搜索词的值进行比较即可。

  比较发现 520987 不相等,则说明 "520""987" 不匹配,则继续向下寻找,这时候该如何做呢?下一步应该比较 "520""876" 了,那么我们如何利用前一步的信息呢?首先我们把 987 减去代表字符 "9" 的部分:

987 - ("9"的映射值 * (M 的 n - 1 次方)) = 987 - (9 * (102 次方)) = 987 - 900 = 87

  然后再乘以 M(这里是 10),再加上 "6" 的映射值,不就成了 876 了么:

87 * M + "6"的映射值 = 87 * 10 + 6 = 876

  当然了,由于采用了取模操作,当两个数值相等时,未必是真正的相等,我们需要进行一次细致的检查(再进行一次朴素的字符串比较)。若不匹配,则可以排除掉。继续下一步。

  如果我们要在 ASCII 字符集范围内查找“搜索词”,由于 ASCII 字符集中有 128 个字符,那么 M 就等于 128,比如我们要在字符串 "abcdefg" 中查找 "cde",那么我们就可以将搜索词 "cde" 转化为“("c"的码点 * M + "d"的码点) * M + "e"的码点 = (99 * 128 + 100) * 128 + 101 = 1634917”这样一个数值。

  分析一下这个数值:1634917,它可以代表字符串 "cde",其中:

代表字符 "c" 的部分是“ "c"的码点 * (M 的 n - 1 次方) = 99 * (1282 次方) = 1622016”
代表字符 "d" 的部分是“ "d"的码点 * (M 的 n - 2 次方) = 100 * (1281 次方) = 12800”
代表字符 "e" 的部分是“ "e"的码点 * (M 的 n - 3 次方) = 101 * (1280 次方) = 101”
(n 代表字符串的长度)

  我们可以随时减去其中一个字符的值,也可以随时添加一个字符的值。

  “搜索词”计算好了,那么接下来计算“源串”,取“源串”的前 n 个字符(n 为“搜索词”的长度)"abc",按照同样的方法计算其数值:

("a"的码点 * M + "b"的码点) * M + "c"的码点 = (97 * 128 + 98) * 128 + 99 = 1601891

  然后将该值与“搜索词”的值进行比较即可。

  比较发现 16349171601891 不相等,则说明 "cde""abc" 不匹配,则继续向下寻找,下一步应该比较 "cde""bcd" 了,那么我们如何利用前一步的信息呢?首先去掉 "abc" 的数值中代表 a 的部分:

(1601891 - "a"的码点 * (M 的 n - 1 次方)) = (1601891 - 97 * (1282 次方)) = 12643

  然后再将结果乘以 M(这里是 128),再加上 "d" 的码点值不就成了 "bcd" 的值了吗:

12643 * 128 + "d"的码点 = 1618304 + 100 = 1618404

  这样就可以继续比较 "cde""bcd" 是否匹配,以此类推。

  如果我们要在 Unicode 字符集范围内查找“搜索词”,由于 Unicode 字符集中有 1114112 个字符,那么 M 就等于 1114112,而 Go 语言中使用 16777619 作为 M 的值,167776191114112 大(更大的 M 值可以容纳更多的字符,这是可以的),而且 16777619 是一个素数。这样就可以使用上面的方法计算 Unicode 字符串的数值了。进而可以对 Unicode 字符串进行比较了。

  其实 M 可以理解为进位值,比如 10 进制就是 10128 进制就是 12816777619 进制就是 16777619

转于 http://www.cnblogs.com/golove/p/3234673.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值