2012-03-16 67 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()。最差情況(所有相同字符的字符串)將會是相同的。