1. 程式人生 > 實用技巧 >indexOf實現引申出來的各種字串匹配演算法

indexOf實現引申出來的各種字串匹配演算法

我們在表單驗證時,經常遇到字串的包含問題,比如說郵件必須包含indexOf。我們現在說一下indexOf。這是es3.1引進的API ,與lastIndexOf是一套的。可以用於字串與陣列中。一些面試經常用問陣列的indexOf是如何實現的,但鮮有問如何實現字串的indexOf是如何實現,因為這是很難很難。要知道,我們平時業務都是與字串與陣列打交道,像數字與日期則更加專業(涉及到二進位制,曆法)是通過庫來處理。

我們回來想一下為什麼字串的indexOf為何如此難?這涉及到字首與字尾的問題,或更專業的說,你應該想到字首樹或字尾樹。如果你連這些概念都沒有,你是寫不好indexOf。字串的問題,可以簡單理解為遍歷,分為全部遍歷或跳著查詢。

我們看最簡單的Brute-Force演算法(又被戲稱為boyfirend演算法)。有兩個字串,長的稱之為目標串,短的一般叫模式串。

其演算法思想是從目標串的第一個字串與模式串的第一字串比較,如果相等,移動目標串的索引,將模式串的索引歸零,讓目標串的子串與模式串繼續逐字比較。

Brute-Force演算法


        function indexOf(longStr, shortStr, pos) {

            var i = pos || 0
                /*------------------------------------*/
                //若串S中從第pos(S的下標0<= pos <=StrLength(S))個字元起存在和串T相同的子串,則匹配成功。
                //返回第一個這樣的子串在串S中的下標;否則返回-1

            var
j = 0; while (true) { if (longStr[i + j] == void 0) break if (longStr[i + j] === shortStr[j]) { j++; //繼續比較後一個字元 if (shortStr[j] === void 0) { return i } } else { //重新開始新一輪的匹配 i++; j = 0; } } return -1; //串S中(第pos個字元起)不存在和串T相同的子串 } console.log(indexOf('aadddaa', 'ddd'))

KMP演算法

第二個是大名鼎鼎的“看毛片”演算法,由Knuth,Morris,Pratt三人分別獨立研究出來,其對於任何模式和目標序列,都可以線上性時間內完成匹配查詢,而不會發生退化,是一個非常優秀的模式匹配演算法。它的核心思想是預處理模式串,將模式串構造一個跳轉表,有兩種形式的跳轉表,next與nextval, nextval可以基於next構建,也可以不。

下面這篇文章詳KMP 的工件原理,大家有興趣看看

http://blog.csdn.net/qq_29501587/article/details/52075200

但如何構建next,nextval呢?我搜了許多文章終於找到相關介紹,我彙總在下面的演算法中了。

        function getNext(str) {
            // 求出每個子串的前後綴的共有長度,然後全部整體後移一位,首項為定值-1,得到next陣列:
            //首先可以肯定的是第一位的next值為0,第二位的next值為1,後面求解每一位的next值時,
            //根據前一位的next值對應的字元與當前字元比較,相同,在前一位的next值加1,
            //否則直接讓它與第一個字元比較,求得共有長度
            //比如說ababcabc
            var next = [0] //第一個子串只有一個字母,不用比較,沒有公共部分,為0
            for (var i = 1, n = str.length; i < n; i++) {
                var c = str[i]
                var index = next[i - 1]
                if (str[index] === c) { // a, a
                    next[i] = index + 1
                } else {
                    next[i] = str[0] === c ? 1 : 0 //第一次比較a, b
                }
            }
            // [0, 0, 1, 2, 0, 1, 2, 0]
            next.unshift(-1)
            next.pop();
            // -1, 0 , 0, 1,2 ,0,1,2
            return next
        }

        function getNextVal(str) {
            //http://blog.csdn.net/liuhuanjun222/article/details/48091547
            var next = getNext(str)
                //我們令 nextval[0] = -1。從 nextval[1] 開始,如果某位(字元)與它 next 值指向的位(字元)相同,
                //則該位的 nextval 值就是指向位的 nextval 值(nextval[i] = nextval[ next[i] ]);
                //如果不同,則該位的 nextval 值就是它自己的 next 值(nextvalue[i] = next[i])。
            var nextval = [-1]
            for (var i = 0, n = str.length; i < n; i++) {
                if (str[i] === str[next[i]]) {
                    nextval[i] = nextval[next[i]]
                } else {
                    nextval[i] = next[i]
                }
            }
            return nextval
        }

        /**
         * KMP 演算法分三分,第一步求next陣列,第二步求nextval陣列,第三步匹配
         * http://blog.csdn.net/v_july_v/article/details/7041827
         * 
         * 前兩步的求法
         * http://blog.csdn.net/liuhuanjun222/article/details/48091547
         * 
         */
        function KmpSearch(s, p) {
            var i = 0;
            var j = 0;
            var sLen = s.length
            var pLen = p.length
            var next = getNextVal(p)
            while (i < sLen && j < pLen) {
                //①如果j = -1,或者當前字元匹配成功(即S[i] == P[j]),都令i++,j++      
                if (j == -1 || s[i] == p[j]) {
                    i++;
                    j++;
                } else {
                    //②如果j != -1,且當前字元匹配失敗(即S[i] != P[j]),則令 i 不變,j = next[j]      
                    //next[j]即為j所對應的next值        
                    j = next[j];
                }
            }
            if (j == pLen)
                return i - j;
            else
                return -1;
        }
        console.log(KmpSearch('abacababc', 'abab'))

你可以將這種演算法看成DFA (有窮狀態自動機)的一種退化寫法,但非常晦澀,它是世界第一次打破字串快速匹配的困局,啟迪人們如何跳著匹配字串了。

Boyer-Moore演算法

Boyer-Moore演算法是我們文字編輯器進行diff時,使用的一種高效演算法,比KMP快三到四倍,思想也是預處理模式串,得到壞字元規則和好字尾規則移動的對映表,下面程式碼中MakeSkip是建立壞字元規則移動的對映表,MakeShift是建立好字尾規則的移動對映表。

下面是阮一峰的文章,簡單介紹什麼是壞字串與好字尾,但沒有如何介紹如何實現。

http://www.ruanyifeng.com/blog/2013/05/boyer-moore_string_search_algorithm.html

壞字串還能輕鬆搞定,但好字尾就難了,都是n^2, n^3的複雜度,裡面的迴圈大家估計也很難看懂。。。

           //http://blog.csdn.net/joylnwang/article/details/6785743
          // http://blog.chinaunix.net/uid-24774106-id-2901288.html

        function makeSkip(pattern) { //效率更高
            var skip = {}
            for (var n = pattern.length - 1, i = 0; n >= 0; n--, i++) {
                var c = pattern[n]
                if (!(c in skip)) {
                    skip[c] = i //最後一個字串為0,倒二為1,倒三為2,重複跳過
                }
            }
            return skip
        }

        function makeShift(pattern) {
            var i, j, c, goods = []
            var patternLen = pattern.length
            var len = patternLen - 1
            for (i = 0; i < len; ++i) {
                goods[i] = patternLen
            }

            //初始化pattern最末元素的好字尾值  
            goods[len] = 1;

            //此迴圈找出pattern中各元素的pre值,這裡goods陣列先當作pre陣列使用  
            for (i = len, c = 0; i != 0; --i) {
                for (j = 0; j < i; ++j) {
                    if (pattern.slice(i, len) === pattern.slice(j, len)) {
                        if (j == 0) {
                            c = patternLen - i;
                        } else {
                            if (pattern[i - 1] != pattern[j - 1]) {
                                goods[i - 1] = j - 1;
                            }
                        }
                    }
                }
            }

            //根據pattern中個元素的pre值,計算goods值  
            for (i = 0; i < len; i++) {
                if (goods[i] != patternLen) {
                    goods[i] = len - goods[i];
                } else {
                    goods[i] = len - i + goods[i];

                    if (c != 0 && len - i >= c) {
                        goods[i] -= c;
                    }
                }
            }
            return goods
        }

        function BMSearch(text, pattern) {
            var i, j, m = 0
            var patternLen = pattern.length
            var textLen = text.length
            i = j = patternLen - 1

            var skip = makeSkip(pattern) //壞字元表
            console.log(skip)
            var goods = makeShift(pattern) //好字尾表
            var matches = []
            while (j < textLen) { //j 是給text使用
                //發現目標傳與模式傳從後向前第1個不匹配的位置  
                while ((i != 0) && (pattern[i] == text[j])) {
                    --i
                    --j
                }

                //找到一個匹配的情況  
                var c = text[j]
                if (i == 0 && pattern[i] == c) {
                    matches.push(j)
                    j += goods[0]
                } else {
                    //壞字元表用字典構建比較合適  
                    j += Math.max(goods[i], typeof skip[c] === 'number' ? skip[c] : patternLen)
                }

                i = patternLen - 1 //回到最後一位
            }

            return matches
        }


        console.log(BMSearch('HERE IS ASIMPLE EXAMPLE', 'EXAMPLE'))

對於進階的單模式匹配演算法而言,子串(字首/字尾)的自包含,是至關重要的概念,是加速模式匹配效率的金鑰匙,而將其發揚光大的無疑是KMP演算法,BM演算法使用字尾自包含,從>後向前匹配模式串的靈感,也源於此,只有透徹理解KMP演算法,才可能透徹理解BM演算法。

壞字元表,可以用於加速任何的單模式匹配演算法,而不僅限於BM演算法,對於KMP演算法,壞字元表同樣可以起到大幅增加匹配速度的效果。對於大字符集的文字,我們需要改變壞字元表>的使用思路,用字典來儲存模式串中的字元的跳轉步數,對於在字典中沒有查到的字元,說明其不在模式串中,目標串當前字元直接滑動patlen個字元。

BMH演算法

BMH 演算法是在BM演算法上改進而來,捨棄晦澀複雜的後好綴演算法,僅考慮了“壞字元”策略。它首先比較文字指標所指字元和模式串的最後一個字元,如果相等再比較其餘m一1個字元。無論文字中哪個字元造成了匹配失敗,都將由文字中和模式串最後一個位置對應的字元來啟發模式向右的移動。關於“壞字元”啟發和“好尾綴”啟發的對比,孫克雷的研究表明:“壞字元”啟發在匹配過程中占主導地位的概率為94.O3 ,遠遠高於“好尾綴”啟發。在一般情況下,BMH演算法比BM有更好的效能,它簡化了初始化過程,省去了計算“好尾綴”啟發的移動距離,並省去了比較“壞字元”和“好尾綴”的過程。

演算法思想:

  1. 搜尋文字時,從後到前搜尋;

  2. 如果碰到不匹配時,移動pattern,重新與text進行匹配;

關鍵:移動位置的計算shift_table如下圖所示。

其中k為Pattern[0 ... m-2]中,使Pattern [ k ] ==Text [ i+m-1 ]的最大值;

如果沒有可以匹配的字元,則使Pattern[ 0 ]==Text [ i+m ],即移動m個位置

  1. 如果與Pattern完全匹配,返回在Text中對應的位置;

  2. 如果搜尋完Text仍然找不到完全匹配的位置,則返回-1,即查詢失敗


        function BMHSearch(test, pattern) {
            var n = test.length
            var m = pattern.length
            var shift = {}

            // 模式串P中每個字母出現的最後的下標,最後一個字母除外
            // 主串從不匹配最後一個字元,所需要左移的位數
            for (var i = 0; i < m - 1; i++) {
                shift[pattern[i]] = m - i - 1; //就是BM的壞字母表
            }

            // 模式串開始位置在主串的哪裡
            var s = 0;
            // 從後往前匹配的變數
            var j;
            while (s <= n - m) {
                j = m - 1;
                // 從模式串尾部開始匹配
                while (test[s + j] == pattern[j]) {
                    j--;
                    // 匹配成功
                    if (j < 0) {
                        return s;
                    }
                }
                // 找到壞字元(當前跟模式串匹配的最後一個字元)
                // 在模式串中出現最後的位置(最後一位除外)
                // 所需要從模式串末尾移動到該位置的步數
                var c = test[s + m - 1]
                s = s + (typeof shift[c] === 'number' ? shift[c] : m)
            }
            return -1;
        }
        console.log(BMHSearch('HERE IS ASIMPLE EXAMPLE', 'EXAMPLE'))
        console.log(BMHSearch('missipipi', 'pip'))

Sunday演算法

Sunday演算法思想跟BM演算法很相似,在匹配失敗時關注的是文字串中參加匹配的最末位字元的下一位字元。如果該字元沒有在匹配串中出現則直接跳過,即移動步長= 匹配串長度+1;否則,同BM演算法一樣其移動步長=匹配串中最右端的該字元到末尾的距離+1。

        function sundaySearch(text, pattern) {
            var textLen = text.length
            var patternLen = pattern.length
            if (textLen < patternLen)
                return -1
            var shift = {} //建立跳轉表
            for (i = 0; i < patternLen; i++) {
                shift[pattern[i]] = patternLen - i
            }
            var pos = 0
            while (pos <= (textLen - patternLen)) { //末端對齊
                var i = pos,
                    j
                for (j = 0; j < patternLen; j++, i++) {
                    if (text[i] !== pattern[j]) {
                        var c = text[pos + patternLen]
                        pos += typeof shift[c] === 'number' ? shift[c] : patternLen + 1
                        break
                    }
                }
                if (j === patternLen) {
                    return pos
                }
            }
            return -1

        }

        console.log(sundaySearch('HERE IS ASIMPLE EXAMPLE', 'EXAMPLE'))
        console.log(sundaySearch('missipipi', 'pip'))

廣州品牌設計公司https://www.houdianzi.com PPT模板下載大全https://redbox.wode007.com

Shift-And和Shift-OR演算法

這個演算法已經超出筆者的能力,只是簡單給出連結

http://www.iteye.com/topic/1130001

bitmap演算法思想

這個在騰訊面試題考過,但這個演算法優缺點也太明顯,本文也簡單給出連結,供學霸們研究

http://www.tuicool.com/articles/aYfEvy

更多連結(裡面有更多演算法實現與複雜度介紹)

http://blog.csdn.net/airfer/article/details/8951802/

像我們這樣的平常人怎麼在專案用它們呢,可能在前端比較少用,但也不是沒有,如富文字編輯器,日誌處理,用了它們效能提升一大截。總之,不要天天做輕鬆的事,否則你沒有進步。