計算機演算法:Morris-Pratt字串搜尋演算法

賈洪峰發表於2012-05-26

原貼發表時間:2012年4月9日,作者:Stoimen

前言

我們前面已經看到,強力字串搜尋演算法和Rabin-Karp字串搜尋演算法均非有效演算法。不過,為了改進某種演算法,首先需要詳細理解其基本原理。我們已經知道,強力字串匹配的速度緩慢,並已嘗試使用Rabin-Karp中的一個雜湊函式對其進行改進。問題是,Rabin-Karp的複雜度與強力字串匹配相同,均為O(mn)。

我們顯然需要採用一種不同方法,但為了提出這種不同方法,先來看看強力字串搜尋有什麼不妥之處。事實上,再深入地研究一下它的基本原理,就能找到問題的答案了。

在強力匹配演算法中,檢視文字中的每個字元是否與模式串的第一個字元匹配。如果匹配,則順次比較模式串的第二個字元是否與文字的下一字元匹配。問題在於,當出現失配時,我們必須要在文字中回退若干位置。嗯,這種方法事實上是無法優化的。

在強力字串匹配演算法中,若出現失配,必須回退,並對比已經對比過的字元!

在強力字串匹配演算法中,若出現失配,必須回退,並對比已經對比過的字元!

從上圖可以看出問題所在:一旦出現失配,必須回滾,從正文中一個已經考察過的位置開始比較。在這裡給出的示例中,我們已經查對了第一、二、三、四字母,此時模式串與文字之間出現失配,於是……於是我們就得返回去,從文字的第二個字母重新開始比較。

這一過程顯然沒有任何作用,因為我們已經知道模式串的起始字母為“a”,而且在位置1與位置3之間沒有這一字母。那我們如何消除這種不必要的重複呢?

概述

James H. Morris和Vaughan Pratt在1977年回答了這一問題,他們當時對自己的演算法進行了介紹,這種演算法會跳過大量無用對比,所以其效率高於強力字串匹配。我們來詳細地研究一下。唯一值得注意的地方就是:它利用了在對模式串與可能匹配進行對比期間收集的資訊,如下圖所示。

Morris-Pratt向前移動到下一可能匹配位置,略過一些對比!

Morris-Pratt向前移動到下一可能匹配位置,略過一些對比!

為利用該資訊,必須首先對模式串進行預處理,以獲取後續匹配的可能位置。之後開始查詢可能的匹配結果,在發生失配時,我們可以準確地知道應當跳轉到何處,以跳過沒有任何用處的對比。

生成後續對比位置表格

這是Morris-Pratt演算法中最富有技巧性的地方,這種演算法就是通過這一步驟來克服強力字串搜尋演算法的缺陷的。讓我們來看幾張圖片。

顯然,如果模式串中僅包含不同字元,在發生失配時,應當開始將文字中的下一字元與模式串的第一字元進行對比!

顯然,如果模式串中僅包含不同字元,在發生失配時,應當開始將文字中的下一字元與模式串的第一字元進行對比!

不過,當模式串中存在重複字元時,如果在該字元之後出現失配,則必須從這一重複字元開始查詢可能匹配,如下圖所示。

如果模式串中包含重複字元,則“下一位置”表格會稍有不同!

如果模式串中包含重複字元,則“下一位置”表格會稍有不同!

最後,如果文字中的重複字元不止1個,“下一個”表格將給出其位置。

“下一個”表格中包含重複字元的位置!

“下一個”表格中包含重複字元的位置!

有了這個包含“後續”可能位置的表格之後,就可以開始在文字中查詢模式串了。

enter image description here

實現

Morris-Pratt演算法的實現並不困難。首先,必須對模式串進行預處理,然後執行搜尋。以下PHP程式碼展示了具體過程。

/**
* Pattern
* 
* @var string
*/
$pattern = 'mollis';

/**
* Text to search
* 
* @var string
*/
$text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque eleifend nisi viverra ipsum elementum porttitor quis at justo. Aliquam ligula felis, dignissim sit amet lobortis eget, lacinia ac augue. Quisque nec est elit, nec ultricies magna. Ut mi libero, dictum sit amet mollis non, aliquam et augue';

/**
* Preprocess the pattern and return the "next" table
* 
* @param string $pattern
*/
function preprocessMorrisPratt($pattern, &$nextTable)
{
    $i = 0;
    $j = $nextTable[0] = -1;
    $len = strlen($pattern);

    while ($i < $len) {
        while ($j > -1 && $pattern[$i] != $pattern[$j]) {
            $j = $nextTable[$j];
        }

        $nextTable[++$i] = ++$j;
    }
}

/**
* Performs a string search with the Morris-Pratt algorithm
* 
* @param string $text
* @param string $pattern
*/
function MorrisPratt($text, $pattern)
{
     // get the text and pattern lengths
    $n = strlen($text);
    $m = strlen($pattern);
    $nextTable = array();

    // calculate the next table
    preprocessMorrisPratt($pattern, $nextTable);

    $i = $j = 0;
    while ($j < $n) {
         while ($i > -1 && $pattern[$i] != $text[$j]) {
            $i = $nextTable[$i];
        }
        $i++;
        $j++;
        if ($i >= $m) {
            return $j - $i;
        }
    }
    return -1;
  }

 // 275
 echo MorrisPratt($text, $pattern);

複雜度

這一演算法需要一定的時間和空間進行預處理。模式串的預處理可以在O(m)內完成,其中m為模式串的長度,而搜尋本身需要O(m+n)。好訊息是預處理過程只需要完成一次,然後就可以根據需要執行任意次搜尋了!

下面的圖表給出了5字母模式串的O(n+m)複雜度,並將其與O(nm)進行對比。

enter image description here

應用

優點

  1. 其搜尋複雜度為O(m+n),快於強力演算法和Rabin-Karp演算法
  2. 其實現相當容易

缺點

  1. 需要額外的空間與時間-O(m)進行預處理
  2. 可以稍加優化(Knuth-Morris-Pratt)

結語

顯然,這一演算法非常有用,因為它以一種非常雅緻的方式對強力匹配演算法進行了改進。另一方面,我們必須知道還有諸如Boyer-Moore演算法等更快速的字串查詢演算法。不過,Morris-Pratt演算法在許多情況下都非常有用,所以理解其基本原理後可能會非常便利。

相關貼子:

  1. 計算機演算法:Boyer-Moore字串查詢法
  2. 計算機演算法:Rabin-Karp字串查詢法
  3. 計算機演算法:強力字串查詢法

原文標題:Computer Algorithms: Morris-Pratt String Searching 原文連結:http://www.stoimen.com/blog/2012/04/09/computer-algorithms-morris-pratt-string-searching/

相關文章