1. 程式人生 > >面試演算法之字串匹配演算法,Rabin-Karp演算法詳解

面試演算法之字串匹配演算法,Rabin-Karp演算法詳解

既然談論到字串相關演算法,那麼字串匹配是根本繞不過去的坎。在面試中,面試官可能會要你寫或談談字串的匹配演算法,也就是給定兩個字串,s 和 t, s是要查詢的字串,t是被查詢的文字,要求你給出一個演算法,找到s在t中第一次出現的位置,假定s為 acd, t為acfgacdem, 那麼s在t中第一次出現的位置就是4.

字串匹配演算法有多種,每種都有它的優點和缺陷,沒有哪一種演算法是完美無缺的。如果在面試中被問到這個問題,最好的處理方法是先詳細的給出一個具體演算法,然後再去大概的探討其他方法的優劣,做到這一點,通過的勝算就相當大了,由此,我們需要了解主流的字串匹配演算法。我們先總結一下常用匹配演算法的特點:

演算法 預處理時間 匹配時間
暴力匹配法 O(m) O((n-m+1)m)
Rabin-Karp O(m) O((n-m+1)m
Finite automaton O(m ||) O(n)
Knuth-Morris-Pratt O(m) O(n)

上標中,m是匹配字串s的長度,n是被匹配文字t的長度,符號, 表示的是文字的字符集,如果文字是二進位制檔案,那麼文字t和s只有兩個字元即0,1組成,那麼 ={0,1}. 如果t和s表示的是基因序列那麼 = {A, C, G, T},如果t和s是由26個英文字元組成那麼,那麼

= {a,b … z}.

同時符號 | | 表示文字長度,例如s=”abcd”, 那麼|s| 就等於4.所以,如果 = {a,b … z}, 那麼|| 就等於26.

大家如果對字串匹配演算法比較瞭解的話,一定對KMP演算法早有耳聞,雖然它在理論上的時間複雜度是最好的,但這並不意味著,它就是最好的演算法,因為它實現起來比較複雜,因此在大多數情況下,其他演算法往往優於KMP,並且在很多運用情形下,其他演算法的執行效率未必比KMP差多少。

我們需要定義幾個概念:
1. 字首,如果字串w是x的字首,那意味著x可以分解成兩個字串的組合, x = wy, 例如 w=ab, x =abcd, 於是x可以分解成w=ab , y = cd兩個字串的組合,如果w是x的字首,那麼|x| <= |w|
2. 字尾,如果字串w是x的字尾,那就是x可以分解成兩個字串的組合,x=yw, 而且有 |w| <= |x|

還有一個簡單的定理:
有三個字串x, y ,z. 如果x, y 都是z 的字首,那麼當|x| < |y| 時,x是y的字首,如果|x|>|y| ,那麼y是x的字首。如果x,y是z的字尾,那麼也同理可證。

如果字串P含有m個字元,記為P[1…m], 那麼P的長度為k的字首P[1…k], 記做Pk. 於是 P0 = ϵ, Pm= P = P[1…m].同理,對於被匹配的文字T,長度為n, T的k字元長度的字首也可以用Tk 來表示,於是,如果要查詢P是否是T的子串,那麼,我們需要找到一個值s, 0<=s<=n - m. 使得 P 是 Ts+m 的字尾。

上面的定義有點燒腦,大家可以拿筆畫畫,以便於理解。

暴力列舉法

列舉法的流程是,在範圍[0, n-m] 中,查詢是否存在一個值s, 0<=s<=n-m, 使得 P[1…m] = T[s+1, … , s+m].java程式碼如下:

int match(String p, String t) {
    for (int s = 0; s < t.length() - p.length(); s++) {
        for (int i = 0; i < p.length(); i++) {
              if (p.charAt(i) == t.charAt(s+i) 
              && i == p.length() - 1) {
                   return s;
              } else if(p.charAt(i) != t.charAt(s+i)){
                   break;
             }
         }
    }

    return -1;
}

如果t = “acaabc”, p = “aab”, 那麼上面演算法執行流程如下:

 a  c  a  a  b  c
 a  a  b

 a  c  a  a  b  c
    a  a  b

 a  c  a  a  b  c
       a  a  b  (成功匹配)

程式碼中,最壞情況下,外層的for迴圈次數為m - n + 1 次,內層for迴圈執行 m 次,所以列舉法的演算法複雜度是O((m-n+1) m)。列舉法的效率很低,因為在匹配過程中,它完全忽略了P的自身組成特點。後面的演算法之所以效率得以提高,很大程度上,就是重複考慮了P自身的字元組合特點。

Rabin-Karp演算法

該演算法在實際運用中,表現不錯,RK演算法需要O(m) 時間做預處理,在最壞情況下,該演算法的複雜度與列舉法一樣都是,O((n - m + 1) m).但在實際運用中,最壞情況極少出現。

假設字符集全是由0到9的數字組成, = {0,1,2…9}.一個長度為k的字串,例如k=3的字串”123”, “404”, 等,都可以看成是含有k個數字的整形數,例如前頭兩個字串可看做兩個整形數:123, 404.由此,對於一個長度為m的字串P[1…m],用p表示該字串對應的含有m個數字的整形數,我們用ts 來表示T[s+1, … , s+m] 這m個字元對應的整形數值,不難發現,當兩個數值相等時,這兩個數值對應的字串就相等,也就是當且僅當p = ts 有 P[1…m] = T[s+1,…,s+m].

把數字從字串轉換為對應的整形數值,我們前頭講過,時間複雜度為O(m).通過下面的公式進行轉換即可:

p = P[m] + 10(P[m-1] + 10(P[m-2]+ …. + ( 10(P[2]) + P[1])…).

RK演算法有一個巧妙之處在於如何通過ts 去計算ts+1.假設m=5,
T=”314152”, 那麼可以算出t0 = 31415. t1的數值可以通過一步運算得出: t1 =10* (t0 - 1051T[1]) + T[6] = 10(31415 - 1051 * 3) + 2 = 14152. (請大家忽略公式中的那根豎線 |, 這根豎線應該是部落格編輯器的bug).

由此可以得到通用公式: ts+1 =10* (ts - 10m1 * T[s+1]) + T[s + m + 1], 0 <= s <= n - m

由於一次計算的複雜度是O(1), 計算t0, t1, … , tnm ,所需要的時間複雜度是O(n - m + 1). 從而,要在 T[1…n] 中查詢P[1..m], 所需要的時間複雜度就是O(n - m + 1).

雖說我們當前處理的是數字字串,如果處理的文字是小寫字元{a,b…z}, 其實本質是一樣的,只要把十進位制的數值{0,1..9}換成26進位制的數字{0, 1, 2, ….25},那面公式中的10換成26即可。

上面演算法,含有一個問題,那就是當m過大,於是對應的數值p或ts過大,會導致溢位,同時就如以前在二進位制演算法章節中談到過,當兩個過大的數值比較大小時,CPU需要多個運算週期來進行,這樣兩數比較,我們就不能假定他們可以在單位時間內完成了。處理這種情況的處理辦法就是求餘。

ts+1 = (d*(ts - T[s+1] * h ) + T[s+m+1]) mod q

其中, h dm1 (mod q), h的值可以預先通過O(m)次計算得到, q 是一個素數。

然而引入求餘又會引發新的問題,ts p (mod q), 並不意味著就一定有 ts = p. 但相反,ts ! p (mod q),那麼一定有 ts != p. 由此,一旦ts p (mod q) 滿足,我們還需要把T[s+1, … , s+m] 和 P[1…m] 這兩個字串逐個字元比較,每個字元都一樣,才能最終斷定T[s+1,…,s+m] = P[1…m].

這就解釋了為何RK演算法最壞情況下,複雜度會是O(m (n - m + 1)). 因為有可能出現這樣情況,ts p (mod q), 但T[s+1, … s+m] 與 P[1…m]不匹配。

舉個具體例子看看:
T = “2359023141526739921”, q = 13, d = 10, P=31415,m=5,不難發現s = 6 的時候,滿足T[s+1, … s+m+1] = P[1..5], 但是當s=12時,T[s+1, …, s+m+1] = “67399”, 然而7 67399 31415 (mod 13). 但是字串”67399” 與字串 “31415”並不匹配。

下面我們看看java實現程式碼:


public class RabinKarp {
    private String  T ;
    private String  P ;
    private int d;
    private int q;
    private int n = 0;
    private int m = 0;
    private int h = 1;
    //假設要匹配的文字字符集是小寫字母{a...z}

    public RabinKarp(String t, String p, int d, int q) {
        T = t;
        P = p;
        this.d = d;
        this.q = q;
        n = T.length();
        m = P.length();

        for (int i = 0; i < m-1 ; i++) {
            h *= (d );
            h = (h % q);
        }
    }

   public int match() {
       int p = 0;
       int t = 0;

       //預處理,計算p 和 t0.
       for (int i = 0; i < m; i++) {
           p = (d*p + (P.charAt(i) - 'a')) % q;
           t = (d*t + (T.charAt(i) - 'a')) % q;
       }

       for (int s = 0; s <= n - m; s++) {

           if (p == t) {
               //如果求餘後相等,那麼就逐個字元比較
               for (int i = 0; i < m; i++) {
                   if (i == m-1 && P.charAt(i) == T.charAt(s+i)) {
                       return s;
                   } else if (P.charAt(i) != T.charAt(s+i)){
                       break;
                   }
               }
           }

           if (s <= n - m) {

               t = (d*(t-(T.charAt(s) - 'a')*h) + T.charAt(s+m) - 'a') % q;

               if (t < 0) {
                   t += q;
               }
           }
       }

       return -1;
   }
}

public class ArrayAndString {

    public static void main(String[] args) {
        String T = "abcabaabcabac";
        String P = "abaa";
        RabinKarp rk = new RabinKarp(T, P, 26, 29);
        int s = rk.match();

        System.out.println("Valid shift is: "+ s);
    }
}

在match 呼叫中,一開始就先計算p 和 t0, 在上面的for迴圈中,不斷的在ts 基礎上計算ts+1, 在程式碼中,需要注意的是,等式中有:t-(T.charAt(s) - ‘a’)*h, 由於每次計算完ts後,我們都會將結果與q求餘,這就使得ts的值不會大於q, 這樣在下一次迴圈計算ts+1時,t-(T.charAt(s) - ‘a’)*h 這一部分的運算結果會有負值出現,在求餘運算下,負值不會影響最終結果,但對計算的值需要做一些修正,例如當q = 29 時, -1 28 (mod 29), 所以如果等式計算出負值時,我們需要修正一下,將負值加上q,得到等價的正值,例如-1等價的正值是28,所以-1 + 29就等於28.

上面的程式碼執行後,輸出結果s 等於3, 也就是T[3,..6] = P[1..4].執行結果顯示程式碼實現,基本正確。

後面的兩種演算法,將在後續章節中,我們再深入研究。

相關推薦

面試演算法字串匹配演算法Rabin-Karp演算法

既然談論到字串相關演算法,那麼字串匹配是根本繞不過去的坎。在面試中,面試官可能會要你寫或談談字串的匹配演算法,也就是給定兩個字串,s 和 t, s是要查詢的字串,t是被查詢的文字,要求你給出一個演算法,找到s在t中第一次出現的位置,假定s為 acd, t為a

隱馬爾科夫演算法實現簡易版的拼音輸入法程式碼

這段時間瞭解了隱馬爾科夫演算法,然後拼音輸入法的核心就是HMM,然後從github上找了一個輸入法實現的程式碼來更透徹的理解演算法,本文程式碼來源:https://github.com/LiuRoy/Pinyin_Demo,如果侵權,請聯絡我刪除!!! 一、 拼音輸入法的原理概述 1.主要原

高效面試字串匹配(KMP,AC演算法

3.AC 多模匹配演算法  看下面這個例子:給定5個單詞:say she shr he her,然後給定一個字串yasherhs。問一共有多少單詞在這個字串中出現過。 三步:構建trik樹,給trik樹新增失敗路徑,建立AC自動機,根據AC自動機搜尋文字 1.構建trik樹  1constint ki

Rabin-karp演算法實現 字串匹配

// RabinKarp演算法實現 // RabinKarp演算法實現 const primeRK = 16777619 func hashStr(seq string) (uint32, uint32) { hash := uint32(0) for _, value

演算法字串匹配問題

我最近複習一道困難程度的演算法題,發現了許多有趣之處。在借鑑了他人解法後,發現從最簡單的情況反推到原題是一種解鎖新進階的感覺。從遞迴到動態規劃,思維上一步一步遞進,如同一部跌宕起伏的小說,記錄下來和諸君共賞之。 題目如下: 給你一個字串 s 和一個字元規律 p,請你來實現一個支援

C++ Leetcode初級演算法字串中的第一個唯一字元

給定一個字串,找到它的第一個不重複的字元,並返回它的索引。如果不存在,則返回 -1。 案例: s = “leetcode” 返回 0. s = “loveleetcode”, 返回 2. 注意事項:您可以假定該字串只包含小寫字母。 class Solution { pub

貪心演算法+-字串

                思路:兩個字串第i個‘-’的距離 (i從1到字串的‘-’的個數),然後加和即可。(思路還是說不出來,看程式碼)            #include <std

PS圖層混合演算法四(亮光 點光 線性光 實色混合)

亮光模式: 根據繪圖色通過增加或降低“對比度”,加深或減淡顏色。如果繪圖色比50%的灰亮,影象通過降低對比度被照亮,如果繪圖色比50%的灰暗,影象通過增加對比度變暗。   線性光模式:根據繪圖色通過增

KMP演算法字串匹配演算法

KMP演算法主要是要計算匹配字元的字首表(prefix table), 舉例:如下面字串的字首表就是陰影框框中的部分。利用字首表來進行匹配  例子:(匹配字元)p=ABABCABAA, (待匹配字元)t=ABABABABCABAAB 具體主要就是求字首表,然後將

ACM經典演算法字串處理

一、(字串替換) 語法:replace(char str[],char key[],char swap[]); 引數: str[]: 在此源字串進行替換操作 key[]: 被替換的字串,不能為空串 swap[]: 替

PS圖層混合演算法五(飽和度色相顏色亮度)

飽和度模式: HcScYc =HBSAYB 飽和度模式:是採用底色的亮度、色相以及繪圖色的飽和度來建立最終色。如果繪圖色的飽和度為0,則原圖沒有變化。 輸出影象的飽和度為上層,色調和亮度保持為下層。

基礎演算法字串轉整數(Leetcode-8)

春招第一步,演算法伴我行 計劃著每天研究幾道演算法題,各個型別儘可能都包含,自己寫出來,好做增強。基本都使用python語言編寫,主要講一下自己的思路,以及AC情況。水平不夠,大家多指正,不吝賜教,十分感謝。 想起之前頭條面試的一道演算法題(另一道下次說),字串轉整數,之前有做過,但是面

《機器學習實戰》學習筆記(四)Logistic(上)基礎理論及演算法推導、線性迴歸梯度下降演算法

轉載請註明作者和出處:http://blog.csdn.net/john_bh/ 執行平臺: Windows Python版本: Python3.6 IDE: Sublime text3 一、概述 Logistic迴歸是統計學習中的經典

演算法分析】字串匹配:BF、KMP演算法

字串匹配演算法:BF、KMP演算法程式碼。 /***************************************** Copyright (c) 2015 Jingshuang Hu @filename:demo.c @datetime:20

字串匹配(BF,BM,Sunday,KMP演算法解析)

字串匹配一直是計算機領域熱門的研究問題之一,多種演算法層出不窮。字串匹配演算法有著很強的實用價值,應用於資訊搜尋,拼寫檢查,生物資訊學等多個領域。 今天介紹幾種比較有名的演算法: 1. BF 2. BM 3. Sunday 4. K

快速字串匹配一: 看毛片演算法(KMP)

前言 由於需要做一個快速匹配敏感關鍵詞的服務,為了提供一個高效,準確,低能耗的關鍵詞匹配服務,我進行了漫長的探索。這裡把過程記錄成系列部落格,供大家參考。 在一開始,接收到快速敏感詞匹配時,我就想到了 KMP 翻譯過來叫“看毛片“的演算法,因為大學的時候就學過它。聽說到它的效率非常高。把原本字串匹配效率 O(

排程演算法MCT(Minimum Completion Time)演算法

最小完成時間演算法MCT(Minimum CompletionTime)是以任意的順序將任務對映到具有最早完成時間的主機上,它並不保證任務被指派到執行它最快的主機上,而僅關心如何最小化任務完成時間,因而可能導致任務在資源上的執行時間過長,從而潛在地增加了排程跨度。 public void run

資料結構與演算法美專欄學習筆記-雜湊演算法

雜湊演算法的定義和原理 將任意長度的二進位制串對映為固定長度的二進位制串。 這個對映的規則就是雜湊演算法,而通過原始資料對映之後得到的二進位制串就是雜湊值。 設計一個優秀的雜湊演算法需要滿足: 從雜湊值不能反向推匯出原始資料(所以雜湊演算法也叫單向雜湊演算法); 對輸入資料非常敏感,哪怕原始

網遊同步演算法導航推測(Dead Reckoning)演算法

在瞭解該演算法前,我們先來談談該演算法的一些背景資料。大家都知道,在網路傳輸的時候,延遲現象是很普遍的,而在基於Server/Client結構下的網路遊戲的同步也就成了很頭疼的問題,在保證客戶端響應使用者本地指令流暢的情況下,沒法有效的保證的同步的及時性。同樣,在軍方也有類似

【Java進階面試系列三】哥們訊息中介軟體在你們專案裡是如何落地的?【石杉的架構筆記】

歡迎關注個人公眾號:石杉的架構筆記(ID:shishan100) 週一至週五早8點半!精品技術文章準時送上! 一、前情回顧 之前給大家聊了一下,面試時如果遇到訊息中介軟體這個話題,面試官上來可能問的兩個問題: 你們的系統架構中為什麼要引入訊息中介軟體? 系統架構中引入訊息中介軟體有什麼缺點? 關於