【问题标题】:Java indexOf function more efficient than Rabin-Karp? Search Efficiency of TextJava indexOf 函数比 Rabin-Karp 更高效?文本的搜索效率
【发布时间】:2012-04-02 05:27:47
【问题描述】:

几周前,我向 Stackoverflow 提出了一个关于创建一种高效算法来搜索大量文本中的模式的问题。现在我正在使用字符串函数 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 函数快,而且慢了一个数量级。我不知道出了什么问题(如果有的话)。有人对此有想法吗?

谢谢,

艾略特

【问题讨论】:

  • 顺便说一句,BigInteger(31, new Random()) 不返回素数(至少有时)
  • 首先,您正在搜索一个非常短的字符串...
  • 我没有仔细查看代码,也不知道 Rabin-Karb 算法,但in a large chunk of text 暗示该算法针对大文本进行了优化,对于较小的文本可能会更慢。您是否也尝试过使用更大的文本?
  • 你试过用一个非常大的干草堆吗?它的性能优势可能只会在更大的范围内体现出来。
  • @MartínCanaval 据我所知(查看 Java 6 源代码)indexOf 不是本机实现的。它只是普通的旧 Java。

标签: java string algorithm search rabin-karp


【解决方案1】:

我之前回答了这个问题,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”后跟一个“1”。这迫使 String.indexOf 达到其最坏情况的性能。对于这个高度退化的测试,Rabin-Karp 算法比 indexOf 快 15 倍左右。

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

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

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

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

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

因此,在每次迭代中,Rabin-Karp 都会越来越落后。我尝试将哈希算法简化为仅异或字符,但我仍然有一个额外的数组访问和两个额外的数字运算,所以它仍然较慢。

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

在 Wikipedia 上读到 Rabin-Karp 用于检测抄袭后,我拿起了圣经的《路得记》,删除了所有标点符号并将所有内容都小写,剩下不到 10000 个字符。然后我搜索了出现在文本末尾附近的“andthewomenherneighboursgaveitaname”。 String.indexOf 仍然更快,即使只使用 XOR 哈希。但是,如果我删除 String.indexOfs 能够访问 String 的私有内部字符数组的优势并强制它复制字符数组,那么最后,Rabin-Karp 确实更快。

但是,我特意选择了那个文本,因为《路得记》中有 213 个“和”,而“和”有 28 个。相反,如果我只搜索最后一个字符“ursgaveitaname”,那么文本中只有 3 个“urs”,因此 indexOf 更接近最佳情况并再次赢得比赛。

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

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

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

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

【讨论】:

  • 哇。感谢您验证我的结果。当我看到你的第一篇文章时,我认为我做错了什么。感谢您对问题的非常透彻的分析。艾略特
  • Rabin-Karp 如果您有多个(或很多)模式,将会大放异彩。具体来说,当哈希计算的成本变得小于模式的数量时。欧文的回答提到了这一点,但我想对从搜索结果到达这里的其他人接受的答案发表评论。
【解决方案2】:

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

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

【讨论】:

  • @BlueRaja-DannyPflughoeft:很好的收获!固定。
【解决方案3】:

据我了解,Rabin Karp 最适合用于在文本块中搜索多个单词/短语。

考虑一个糟糕的词搜索,用于标记辱骂性语言。

如果您有 2000 个单词的列表,包括派生词,那么您需要调用 indexOf 2000 次,每个单词对应一次。

RabinKarp 通过反向搜索来帮助解决这个问题。 对 2000 个单词中的每一个单词进行 4 个字符的哈希,然后将其放入字典中进行快速查找。

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

如您所见,搜索现在是相反的 - 我们正在搜索 2000 个单词以寻找可能的匹配项。 然后我们从字典中获取字符串并做一个等号检查以确保。

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

现在,想象一下进行所有这些 indexOf 搜索的最坏情况 - 我们检查的最后一个词是匹配的......

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

【讨论】:

    【解决方案4】:

    但这是很自然的事情!
    首先,您的测试输入太琐碎了。

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

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

    另外请注意,当使用更复杂的字符串搜索算法时,它们可能会进行“昂贵的”设置/预处理以提供真正快速的搜索。
    在您的情况下,您只需在句子中搜索was。在任何情况下,您都应该始终考虑输入

    【讨论】:

    • 对于更大的随机数字符串,我的结果显示 RK 的表现更差。请参阅我上面的评论。
    【解决方案5】:

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

    • 您很可能仅在非常特殊的情况下才能胜过标准 API 实现。我不认为“玛丽有一只小羊羔,它的羊毛像雪一样白”。
    • 微基准测试非常困难,并且会给出相当误导的结果。 Here is an explanation, here a list of tools you could use

    【讨论】:

      【解决方案6】:

      不仅简单地尝试更长的静态字符串,而且尝试生成随机长字符串,每次都将搜索目标插入到随机位置。如果不对其进行随机化,您将看到indexOf 的固定结果。

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

      【讨论】:

      • 这是一个更无意义的指标 - 在现实世界中,you 多久对完全随机的字符串进行子字符串搜索?
      【解决方案7】:

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

      【讨论】:

        猜你喜欢
        • 2019-10-18
        • 1970-01-01
        • 2016-01-19
        • 2020-12-06
        • 2018-11-18
        • 2015-08-14
        • 2018-03-08
        • 2011-09-13
        • 1970-01-01
        相关资源
        最近更新 更多