2012-03-16 73 views
15

几周前,我对Stackoverflow提出了一个问题,即创建一个有效的算法来搜索大块文本中的模式。现在我正在使用String函数indexOf来执行搜索。一个建议是使用拉宾卡普作为替代。我按照以下方法编写了一个小测试程序来测试Rabin-Karp的实现。Java indexOf函数比Rabin-Karp更高效吗?搜索文本的效率

public static void main(String[] args) { 
    String test = "Mary had a little lamb whose fleece was white as snow"; 

    String p = "was"; 
    long start = Calendar.getInstance().getTimeInMillis(); 
    for (int x = 0; x < 200000; x++) 
     test.indexOf(p); 
    long end = Calendar.getInstance().getTimeInMillis(); 
    end = end -start; 
    System.out.println("Standard Java Time->"+end); 

    RabinKarp searcher = new RabinKarp("was"); 
    start = Calendar.getInstance().getTimeInMillis(); 
    for (int x = 0; x < 200000; x++) 
    searcher.search(test); 
    end = Calendar.getInstance().getTimeInMillis(); 
    end = end -start; 
    System.out.println("Rabin Karp time->"+end); 

} 

这里是拉宾,卡普的实现,我使用:

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

public class RabinKarp { 
private String pat; // the pattern // needed only for Las Vegas 
private long patHash; // pattern hash value 
private int M; // pattern length 
private long Q; // a large prime, small enough to avoid long overflow 
private int R; // radix 
private long RM; // R^(M-1) % Q 
static private long dochash = -1L; 

public RabinKarp(int R, char[] pattern) { 
    throw new RuntimeException("Operation not supported yet"); 
} 

public RabinKarp(String pat) { 
    this.pat = pat; // save pattern (needed only for Las Vegas) 
    R = 256; 
    M = pat.length(); 
    Q = longRandomPrime(); 

    // precompute R^(M-1) % Q for use in removing leading digit 
    RM = 1; 
    for (int i = 1; i <= M - 1; i++) 
     RM = (R * RM) % Q; 
    patHash = hash(pat, M); 
} 

// Compute hash for key[0..M-1]. 
private long hash(String key, int M) { 
    long h = 0; 
    for (int j = 0; j < M; j++) 
     h = (R * h + key.charAt(j)) % Q; 
    return h; 
} 

// Las Vegas version: does pat[] match txt[i..i-M+1] ? 
private 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; 
} 

// check for exact match 
public int search(String txt) { 
    int N = txt.length(); 
    if (N < M) 
     return -1; 
    long txtHash; 
    if (dochash == -1L) { 
     txtHash = hash(txt, M); 
     dochash = txtHash; 
    } else 
     txtHash = dochash; 

    // check for match at offset 0 
    if ((patHash == txtHash) && check(txt, 0)) 
     return 0; 

    // check for hash match; if hash match, check for exact match 
    for (int i = M; i < N; i++) { 
     // Remove leading digit, add trailing digit, check for match. 
     txtHash = (txtHash + Q - RM * txt.charAt(i - M) % Q) % Q; 
     txtHash = (txtHash * R + txt.charAt(i)) % Q; 

     // match 
     int offset = i - M + 1; 
     if ((patHash == txtHash) && check(txt, offset)) 
      return offset; 
    } 

    // no match 
    return -1; // was N 
} 

// a random 31-bit prime 
private static long longRandomPrime() { 
    BigInteger prime = new BigInteger(31, new Random()); 
    return prime.longValue(); 
} 

// test client 

} 

拉宾,卡普的实施工作,它返回正确的我正在寻找字符串的偏移量。然而,令我惊讶的是,当我运行测试程序时发生的时间统计。他们是:

Standard Java Time->39 
Rabin Karp time->409 

这真是令人惊讶。不仅Rabin-Karp(至少在这里实现)不比标准的java indexOf字符串函数更快,而且速度更慢一个数量级。我不知道什么是错的(如果有的话)。任何人都有这个想法?

感谢,

埃利奥特

+0

btw,BigInteger(31,new Random())'不会返回素数(至少有时) – 2012-03-16 16:46:07

+0

首先,您正在寻找一个非常短的字符串... – 2012-03-16 16:46:10

+4

我没有彻底查看该代码也不知道Rabin-Karb算法,但“在大量文本中”暗示该算法针对大文本进行了优化,对于较小的文本可能会较慢。你是否尝试过大幅度的文字呢? – Thomas 2012-03-16 16:46:16

回答

20

我早些时候回答了这个问题,艾略特指出我只是错误的。我向社区道歉。

String.indexOf代码没有什么神奇的。它不是本地优化或类似的东西。您可以从String源代码复制indexOf方法,并且运行速度同样快。

我们这里有什么是O()效率和实际效率之间的差异。 Rabin-Karp为长度为N的字符串和长度为M的模式,Rabin-Karp为O(N + M),O(NM)为最差情况。当你研究它时,String.indexOf()也有O(N + M)的最佳情况和O(NM)的最坏情况。

如果文本中包含许多与模式开头部分匹配的部分匹配,Rabin-Karp将保持接近其最佳性能,而String.indexOf则不会。例如,我测试了上面的代码(这次是正确的:-)),然后是单个'1',然后搜索1000'0,然后是单个'1'。这迫使String.indexOf达到最坏的性能。对于这种高度退化的测试,Rabin-Karp算法比indexOf快大约15倍。

对于自然语言文字,Rabin-Karp将保持接近最佳状态,indexOf只会略微恶化。因此,决定性因素是每一步执行操作的复杂性。

在它最内层的循环中,indexOf扫描匹配的第一个字符。在每一次迭代是具有:

  • 增量循环计数器
  • 执行两个逻辑测试
  • 做一个数组访问

在拉宾-卡普每次迭代具有:

  • 递增循环计数器
  • 执行两个逻辑测试
  • 做两个数组访问(实际上是两个方法调用)
  • 更新的散列,其中上述要求9的数值运算

因此在每次迭代拉宾-卡普将进一步下降,进一步的后面。我尝试将散列算法简化为XOR字符,但我仍然有一个额外的数组访问和两个额外的数字操作,所以它仍然较慢。另外,当找到匹配的时候,Rabin-Karp只知道哈希匹配,因此必须测试每个字符,而indexOf已经知道第一个字符匹配,因此有一个较少的测试要做。

在维基百科上阅读过Rabin-Karp被用来检测剽窃之后,我拿走了圣经的露丝书,删除了所有的标点符号,并将所有小写字母都留下了不到10000个字符。然后,我在文本的最后发现了“and thewomenherneighboursgaveitaname”。即使只有XOR散列,String.indexOf仍然更快。但是,如果我删除了String.indexOfs的优点,那就是能够访问String的私有内部字符数组并强制它复制字符数组,然后,最终,Rabin-Karp真的更快了。

但是,我故意选择那个文本,因为在“路得书28”中有213个“和”s“和”s“。如果我只搜索最后一个字符“ursgaveitaname”,那么在文本中只有3个“urs”,所以indexOf返回接近其最佳情况并再次赢得比赛。

作为一个更公平的测试,我从文本的后半部分中随机选择20个字符串并对它们进行计时。 Rabin-Karp比在String类之外运行的indexOf算法慢大约20%,比实际的indexOf算法慢70%。因此,即使在使用情况下它应该适用于它,它仍然不是最好的选择。

那么拉宾卡普有什么好处呢?不管被搜索的文本的长度或性质如何,每个比较的字符都会变慢。无论我们选择什么样的哈希函数,我们都需要额外的数组访问和至少两个数值运算。一个更复杂的散列函数可以减少错误匹配,但需要更多的数值运算符。拉宾卡普根本无法跟上。

如上所示,如果我们需要找到以经常重复的文本块为前缀的匹配,indexOf可能会变慢,但如果我们知道我们正在这样做,看起来我们仍然会更好地使用indexOf来搜索没有前缀的文本,然后检查前缀是否存在。

根据我今天的调查,我看不到任何时候Rabin Karp的额外复杂性将会得到回报。

+4

哇。感谢您验证我的结果。当我看到你的第一篇文章时,我一直在想我做错了什么。谢谢你对这个问题的深入分析。 Elliott – Elliott 2012-03-17 01:03:10

+1

如果您有多个或多个模式,Rabin-Karp会发光。具体来说,当散列计算的成本变得小于模式的数量时。 Owen的回答中提到了这一点,但我想对其他人从搜索结果中得到的答案留下评论。 – 2016-03-14 21:14:01

1

但这只是自然发生的!
您的测试输入首先太平凡了。

indexOf返回was搜索小缓冲区指数(String的内部char array`),而拉宾,卡普必须做预处理,以建立其数据工作,这需要额外的时间。

要查看差异,您必须在一个非常大的文本中测试才能找到表达式。

另外请注意,当使用更复杂的字符串搜索算法时,他们可以有“昂贵的”设置/预处理来提供真正的快速搜索。
在你的情况下,你只是在一个句子中搜索was。任何情况下,你应该总是把输入考虑到

+0

随着更大的随机数字串,我的结果显示RK甚至更糟。看到我上面的评论。 – Elliott 2012-03-16 20:36:49

6

Here是java.lang.String的来源。 indexOf是1770行。

我怀疑是因为你在这么短的输入字符串上使用它,Rabin-Karp算法的额外开销超过了java.lang.String的indexOf似乎天真的实现,你不是看到算法的真实性能。我会建议尝试一个更长的输入字符串来比较性能。

+1

有问题的代码是'1770'行,而不是'1576' – 2012-03-16 17:10:12

+0

@ BlueRaja-DannyPflughoeft:很好!固定。 – mindvirus 2012-03-16 17:50:43

1

没有寻找到细节,原因有二来我的脑海:

  • 你很可能超越标准的API实现只对非常特殊的情况。我不认为“玛丽有一只羊毛像白雪一样的小羊羔”就是这样。
  • microbenchmarking是非常困难的,可以给出相当令人误解的结果。Here is an explanationhere a list of tools you could use
0

不仅简单地尝试一个较长的静态字符串,而是试图产生随机长串并插入搜索目标到一个随机位置,每次。没有随机化,您将看到indexOf的固定结果。

编辑: 随机是错误的概念。大多数文本并非真正随机的。但是您需要很多不同的长字符串才能生效,而不仅仅是多次测试相同的字符串。我相信有很多方法可以从更大的文本源或类似的东西中提取“随机”大字符串。

+0

这是一个更无意义的度量标准 - 您在实际环境中对完全随机字符串进行子字符串搜索的频率如何? – 2012-03-16 17:06:28

+1

你说得对。香农会带我去完成任务。 – 2012-03-16 17:13:08

5

从我的理解中,Rabin Karp最适用于搜索多个单词/短语的文本块。

想想糟糕的单词搜索,指出虐待语言。

如果您有2000个单词的列表(包括派生词),那么您需要调用indexOf 2000次,其中每个单词要尝试查找一个单词。

RabinKarp通过其他方式进行搜索来帮助解决此问题。 对2000个单词中的每个单词做一个4个字符的散列,并将其快速查找放入字典中。

现在,对于搜索文本的每个4个字符,散列和检查字典。你可以看到,搜索现在是相反的 - 我们正在搜索2000个单词来代替可能的匹配。 然后,我们从字典中获取字符串,并执行相等检查以确定。

这也是一种更快的搜索方式,因为我们正在搜索字典而不是字符串匹配。

现在,想象一下在做所有的indexOf搜索的最坏的情况 - 我们检查的最后一个字匹配...

维基百科的文章RabinKarp甚至提到在你所描述的情况自卑。 ;-) http://en.wikipedia.org/wiki/Rabin-Karp_algorithm

0

对于这种搜索,Knuth-Morris-Pratt可能表现更好。特别是如果子字符串不只是重复字符,那么KMP应该优于indexOf()。最差情况(所有相同字符的字符串)将会是相同的。