KMP算法——Javascript實現

騰訊和阿里的筆試剛過去了,裏面有不少題都很值得玩味的。以前Blog積累的不少東西,還要平時看的書,都有很大的幫助。這個深有體會啊!html

例如,騰訊有一道算法題是吃香蕉(好邪惡的趕腳..),一次吃一根或者兩根,50根香蕉能夠有多少種吃法?當時我一看尼瑪,不就是我以前總結過的:遞歸算法,JavaScript實現。裏面的走樓梯的問題,我到如今仍是記得的。(可是爲了抗議我對捲紙的不專業性,我用CoffeeScript實現了算法...感受可能會所以跪下。)而後就是有一道選擇題,考的是Javascript的閉包陷阱,我一看尼瑪,不是我以前總結過的:循環閉包的影響以及其解決方案。我也是如出一轍用setTimeout去模擬的。簡直不能再爽。固然,也不得不說,騰訊到最後也只有這兩題和前端有一點聯繫。前端

相比之下,阿里就好不少了。雖然時間很緊,題目不少,但起碼不會一擡眼全是熟悉的陌生人。印象比較深的是《Javascript設計模式》裏的觀察者模式,還有《Javascript高級程序設計》裏的有關CookieUtil的。。可是,我有一題,徹底不記得如何作了。那就是今天的主角,KMP算法!算法


上面扯淡完畢了。我的博客嘛,爲所欲爲啦。先給參考資料的地址:字符串匹配的KMP算法。這個是阮一峯老師的博文,算是寫的很不錯的了。想看生動形象的博文的同窗能夠直接移步過去。設計模式

那這個用於字符串匹配的KMP算法到底怎麼用的呢。咱們先看看需求:字符串A="BBCABCDABABCDABCDABDE"裏如何快速匹配到a=「ABCDABD」。用僞代碼來寫這些步驟應該是這樣的:閉包

  1. 字符串的首位與子字符串的首位進行匹配,匹配失敗,則字符串後移繼續匹配。匹配成功,則字符串與子字符串一塊兒後移,繼續匹配。
  2. 繼續匹配的過程當中,最理想的狀態即是從頭至尾成功,而後匹配過程也就結束了。假若中途有不匹配的,子字符串就要回滾。

問題來了:子字符串回滾到哪兒?如果回滾到匹配開始的下一位,那固然是能夠的,只不過是作了不少的無用功。因此KMP算法就是爲了這個時候誕生的,能夠有效的提升效率。優化

這裏我用阮老師的一張圖更好的解釋一下。
img
咱們能夠看到,最佳的回滾位置應該是讓子字符串的「C」對應空格。這樣咱們才能夠最優化的處理重複的「AB」這個東西。設計

直接看一個公式:回滾位數 = 已匹配的字符數 - 對應的部分匹配值。咱們能夠看到已經匹配的字符數是6,而後最佳的回滾位數是4,那麼對應的部分匹配值應該是2,那這個2是怎麼來的?code

這就是KMP算法的精華。對於一個字符串:「ABCDABD」htm

  • 前綴有:A,AB,ABC,ABCD,ABCDA,ABCDAB
  • 後綴有:BCDABD,CDABD,DABD,ABD,BD,D
  * "A"的前綴和後綴都爲空集,共有元素的長度爲0;
  * "AB"的前綴爲[A],後綴爲[B],共有元素的長度爲0;
  * "ABC"的前綴爲[A, AB],後綴爲[BC, C],共有元素的長度0;
  * "ABCD"的前綴爲[A, AB, ABC],後綴爲[BCD, CD, D],共有元素的長度爲0;
  * "ABCDA"的前綴爲[A, AB, ABC, ABCD],後綴爲[BCDA, CDA, DA, A],共有元素爲"A",長度爲1;
  * "ABCDAB"的前綴爲[A, AB, ABC, ABCD, ABCDA],後綴爲[BCDAB, CDAB, DAB, AB, B],共有元素爲"AB",長度爲2;
  * "ABCDABD"的前綴爲[A, AB, ABC, ABCD, ABCDA, ABCDAB],後綴爲[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度爲0。

因此咱們最終只要觀察到共有元素的最大長度,便可使用公式。那咱們要實現這個算法,就要取得部分匹配表的算法和回滾算法。那咱們看一下該如何實現。blog

var kmpGetPartMatchLen = function(str){
    var partMatch = [];
    for(var i = 0; i < str.length; i++){
        var prefix = "",
            suffix = "";
        var newStr = str.slice(0, i + 1);
        if(newStr.length <= 1){
            partMatch[i] = 0;
        }else{
            //判斷先後綴是否相同
            for(var j = 0; j < i; j++){
                prefix = newStr.slice(0, j + 1);
                suffix = newStr.slice(-j - 1); //利用負參數尾巴開始取
                if(prefix === suffix){
                    partMatch[i] = prefix.length;
                }
            }
            //不存在檢測
            partMatch[i] = partMatch[i]? partMatch[i] : 0;
        }
    }
    return partMatch;
};

上面這個是取出部分匹配表的算法的實現,而後接下來就是回滾算法的實現。

var kmp = function(sourceStr, subStr){
    var partMatch = kmpGetPartMatchLen(subStr);
    var result = false;

    for(var i = 0; i < sourceStr.length; i++){
        for(var j = 0; j < subStr.length; j++){
            if(subStr.charAt(j) === sourceStr.charAt(i + j)){
                if(j === subStr.length - 1){
                    result = true;
                    break;
                }
            }else{
                //實現回滾,以subStr爲參照物,即sourceStr往前移動
                if(j > 0 && partMatch[j-1] >= 0){
                     //公式在此處實現
                    i += (j - 1 - partMatch[j-1] - 1);
                }else{
                    break;
                }
            }
        }
        if(result) break;
    }

    if(result){
        return i;
    }else{
        return -1;
    }
};

那回到咱們的筆試題,要實現手機號後四位在π中匹配的位置,那如今就是一句話的事情啦!

var π = "3.1415926.........."
kmp(π, "1092");
相關文章
相關標籤/搜索