Java indexOf函数比Rabin-Karp更有效吗?文本的搜索效率

时间:2012-03-16 16:43:09

标签: java string algorithm search rabin-karp

几周前,我向Stackoverflow提出了一个问题,即创建一个有效的算法来搜索大块文本中的模式。现在我使用String函数indexOf进行搜索。一个建议是使用Rabin-Karp作为替代方案。我按如下方式编写了一个小测试程序来测试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);

}

以下是我正在使用的Rabin-Karp的实现:

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

}

Rabin-Karp的实现工作在于它返回我正在寻找的字符串的正确偏移量。令我惊讶的是,当我运行测试程序时发生的时序统计。他们在这里:

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

这真的很令人惊讶。 Rabin-Karp(至少在这里实现)并不比标准的java indexOf String函数快,它慢了一个数量级。我不知道出了什么问题(如果有的话)。有没有人想过这个?

谢谢,

埃利奥特

7 个答案:

答案 0 :(得分:20)

我早些回答了这个问题,Elliot指出我完全错了。我向社区道歉。

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则不会。例如,我测试了上面的代码(正确的这一次:-))在百万'0'后跟一个'1',并搜索1000'0's然后是单个'1'。这迫使String.indexOf达到最差的性能。对于这种高度简并的测试,Rabin-Karp算法比indexOf快约15倍。

对于自然语言文本,Rabin-Karp将保持接近最佳情况,而indexOf只会略有恶化。因此,决定因素是每个步骤执行的操作的复杂性。

在最里面的循环中,indexOf会扫描匹配的第一个字符。每次迭代都必须:

  • 增加循环计数器
  • 执行两次逻辑测试
  • 进行一次数组访问

在Rabin-Karp中,每次迭代都必须:

  • 增加循环计数器
  • 执行两次逻辑测试
  • 进行两次数组访问(实际上是两次方法调用)
  • 更新哈希,上面需要9个数值运算

因此,在每次迭代中,Rabin-Karp将进一步落后。我尝试将哈希算法简化为XOR字符,但我仍然有额外的数组访问和两个额外的数值运算,因此它仍然较慢。

此外,当找到匹配时,Rabin-Karp只知道哈希匹配,因此必须测试每个字符,而indexOf已经知道第一个字符匹配,因此只需要少一个测试。

在维基百科上读到Rabin-Karp被用来检测剽窃,我拿了圣经的露丝书,删除了所有标点符号,并将所有小写字母留下了不到10000字符。然后我搜索了“andthewomenherneighboursgaveitaname”,它出现在文本的最末端。 String.indexOf仍然更快,即使只有XOR哈希。但是,如果我删除String.indexOfs的优点是能够访问String的私有内部字符数组并强制它复制字符数组,那么,最后,Rabin-Karp真的更快。

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

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

Rabin-Karp有什么用?无论要搜索的文本的长度或性质如何,在每个角色进行比较时都会更慢。无论我们选择什么哈希函数,我们肯定需要进行额外的数组访问和至少两个数值运算。更复杂的散列函数将减少错误匹配,但需要更多的数字运算符。拉宾卡普根本没办法跟上。

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

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

答案 1 :(得分:6)

Here是java.lang.String的源代码。 indexOf是第1770行。

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

答案 2 :(得分:5)

根据我的理解,Rabin Karp最适合在搜索多个单词/短语的文本块时使用。

考虑一个糟糕的单词搜索,用于标记滥用语言。

如果你有一个包含派生词的2000个单词的列表,那么你需要调用indexOf 2000次,每个单词都要找一个。

RabinKarp以相反的方式进行搜索来帮助解决这个问题。 对2000个单词中的每个单词进行4个字符的散列,并将其放入具有快速查找的字典中。

现在,对于搜索文本的每4个字符,哈希并检查字典。

正如您所看到的,搜索现在是另一种方式 - 我们正在搜索2000个单词以寻找可能的匹配。 然后我们从字典中获取字符串并做一个等于检查以确定。

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

现在,想象一下做所有indexOf搜索的最糟糕案例 - 我们检查的最后一个词是匹配...

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

答案 3 :(得分:1)

但这很自然地发生! 首先,你的测试输入太微不足道了。

indexOf返回was搜索小缓冲区(String的内部char数组')的索引,而Rabin-Karp必须进行预处理设置其数据工作需要额外的时间。

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

另请注意,当使用更加软化的字符串搜索算法时,他们可以使用昂贵的#34;设置/预处理以提供真正快速的搜索 在您的情况下,您只需在句子中搜索was。无论如何,你应该总是把输入考虑在内

答案 4 :(得分:1)

不考虑细节,我想到了两个原因:

答案 5 :(得分:0)

不仅可以尝试更长的静态字符串,还可以尝试生成随机长字符串,并每次将搜索目标插入随机位置。如果没有随机化,您会看到indexOf的固定结果。

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

答案 6 :(得分:0)

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