字符串匹配之BoyerMoore算法

說明:BoyerMoore算法是一種能快速檢測出在字符串中的與之匹配的算法,該算法廣泛用於各種字符串檢測。網上也有很多解釋該算法的,進過我自己的研究、總結,可能會與網上的版本在代碼和匹配算法上有點區別,但我覺得用自己的方法能更好的理解。(PS:着只是個人觀點哈! 如果覺得沒有什麼差的,或說不懂的,大家每個人都不一樣嘛,總之理解就好。)

過程:想要使用BoyerMoore算法,必須先理解兩種字符串的匹配方法:壞字法和最好後綴法。這兩種方法在不同的情況下效果也不太一樣。

首先我們來通過實例來說明說明是壞字檢測法和最好後綴法:


如圖:需要檢測的字符串是“this is a simple java program”,用來檢測的字符串是“simple”。所謂的壞字就是從後往比較與test字符串中第一個沒有匹配到的字符(至於爲什麼要從後往前,這樣能減少比較的次數:字符串是往後移動的,如果後面的都已經匹配不到了,那前面的也就沒有比較的必要了吧!!)。


如圖" i "就是壞字,匹配不到了,那麼我們就開始移動,移動一個?還是能更多?移動一個顯然不是我們想要的要增加效率顯然要跳過儘可能多的位置。根據上面我說的,其實我們可以把test中的" i "移動至example的i下,這樣我們就成功移動了4個位置,而且這絕對不會漏過什麼。


具體的移動位數爲:壞字對應的在test中的位置-該位置前的在test中與該壞字匹配的位置(如果沒有則將字符串整個移動到壞字之後)。

接下來介紹好後綴法:


顧名思義好後綴法就是當有一個或多個example中的字符與test中的字符中對應,如圖好後綴就是" mple ",那好了現在我們頭腦風暴下最多能移動幾個,移動的規則是什麼?

顯然最多的應該是把" simple "中的" mple "移動到" mple "下,這樣能做到最大的移動位數。

那好,我們該怎麼確定移動的規則,這裏引入一個準備數組suff[i],該數組表示若以當前的以i作爲最後的字符串與test字符串一樣的個數,可能有點繞,就是在對比的時候有一個好後綴,接下來要移動,那麼就要尋找一樣的字符串,假設好後綴長度爲4,如果suff[i]>4的話就可以認爲以i結尾的字符串可以移動到最後。

舉個例子:假設test的字符串是abcbaab

test a b c b a a b
i 0 1 2 3 4 5 6
suff[i] 0 2 0 1 0 0 7

當然suff[6]是沒有用的,假設好後綴是ab,那麼就在suff中從後往前找大於2的數對應的i(不包括最後一個),接下來就是計算移動的個數:test的長度-對應的i-1。
那如果有好後綴但是沒有與之匹配的字符串怎麼辦?
如圖:好後綴是cab但字符串中沒有對應的,那是把它全部移走?那就錯了着是不安全的移動:所謂不安全的就是移動過後可能錯過與之匹配的字符串。看圖test中的前兩個是ab而後綴是cab但是由於ab前面沒有字符了那麼安全的移動就是把ab移動到example的ab下。


爲了解決這種情況,在判斷好後綴是應該多一個if判斷是不是這種情況。
好了瞭解了這兩種字符串的比較,那麼boyermoore算法又是什麼呢?其實就是這兩種算法的結合,在匹配過程中判斷哪個比較方式能跳過更多的字符,那就用哪種方式。
代碼:
import java.util.Scanner;  


public class BoyerMoore {  
    private char[] array;  
    private char[] test;  
    private int ltest, larray;  
    private int[] suff;  
    // 記錄跳過的個數  
    private int flag;  
  
    public BoyerMoore(String s1, String s2) {  
        array = s1.toCharArray();  
        larray = array.length;  
        test = s2.toCharArray();  
        ltest = test.length;  
        flag = 0;  
    }  
  
    // a爲壞字 k爲壞字對應到test中到位置  
    public int checkTest1(char a, int k) {  
        for (int i = k - 1; i >= 0; i--) {  
            if (test[i] == a) {  
                return k - i; // 若檢測到了壞字 移動的個數  
            }  
        }  
        return k + 1; // 沒有檢測到壞字 則移動整個數組  
    }  
  
    // 使用壞字法 跳過的字符個數  
    public int preBmBc() {  
        for (int i = ltest - 1; i >= 0; i--) { // 從後檢測到第一個壞字  
            if (test[i] != array[flag + i]) {  
                return checkTest1(array[flag + i], i);  
            }  
        }  
        return 0; // 匹配到了字符串  
    }  
  
    // 好後綴用到的數組 用來表示以當前位置作爲結尾的字符串最多能與該字符串匹配到的最多的字符  
    public int checkTest2(int i) {  
        int counter = 0;  
        for (int j = ltest - 1; i >= 0; i--, j--) {  
            if (test[i] != test[j])  
                break;  
            counter++;  
        }  
        return counter;  
    }  
  
    // 所生成的數組表示:若以當前的以i作爲最後的字符串與test字符串一樣的個數  
    public void setsuff() {  
        suff = new int[ltest];  
        for (int i = 0; i < ltest; i++) {  
            suff[i] = checkTest2(i);  
        }  
    }  
  
    // 用來檢測有沒有好後綴或者好前綴(好前綴是test裏的前幾個與test裏好後綴的後幾個匹配)  
    public int checkTest3(int length) {  
        for (int i = 0; i < ltest - 1; i++) { // 從後往前尋找 找到suff裏大於好後綴長度的序號  
            if (suff[i] >= length)  
                return ltest - i - 1; // 返回有好後綴時到跳躍個數  
        }  
        for (int i = length - 2; i >= 0; i--) { // 沒有好後綴  
                                                // 所以前length(0到length-1)不可能一樣  
                                                // 故下標從length-2開始找好前綴  
            for (int j = i, k = ltest - 1; j >= 0; j--, k--) {  
                if (test[j] != test[k]) // 找出好前綴的長度  
                    break;  
                return ltest - 1 - i; // 返回跳躍個數  
            }  
        }  
        return ltest;  
    }  
  
    public int preBmGs() {  
        setsuff();  
        int counter = 0;  
        // 記錄最好後綴的個數  
        for (int i = ltest - 1, j = flag + ltest - 1; i >= 0; i--, j--) { 
//        <span style="white-space:pre">	</span>System.out.println(flag);
            if (test[i] != array[j])  
                break;  
            counter++;  
        }  
        if (counter == 0) // 沒有好後綴  
            return 1;  
        if (counter != ltest)  
            return checkTest3(counter);  
        return 0;   
    }  
  
    public int max(int a, int b) {  
        if (a > b)  
            return a;  
        return b;  
    }  
  
    // 使用遞歸查找 結束條件是 被檢測的字符串中沒有 或者是找到該字符串  
    public void find() {  
        // System.out.println(preBmBc()+","+preBmGs());  
        int jump = max(preBmBc(), preBmGs());  
        if (flag + ltest + jump > larray) {  
            System.out.println("字符串中沒有你想找的字符串!!!");  
            return;  
        }  
//        System.out.println(flag);
        if (jump == 0) {  
            System.out.println("找到你想要的字符串了!!" + "\n" + "從第" + (flag+1) + "個到"  
                    + (flag + ltest) + "個。");  
            flag += ltest;  
            return;  
        }  
        flag += jump;  
        find();  
    }  
  
    public static void main(String[] args) {  
        // 用這個方法輸入內容 空格 tab 會被認爲是輸入結束 自己改進下  
        Scanner sc = new Scanner(System.in);  
        System.out.println("--- 輸入被檢測的字符串 ---");  
        String s1 = sc.next();  
        System.out.println("---輸入你想檢測的字符串---");  
        String s2 = sc.next();  
        BoyerMoore b = new BoyerMoore(s1, s2);  
        b.find();  
    }  
  
}  

最後大家可能看到我和網上提供的方法不太一樣,我解釋下:首先我在處理壞字時沒有用到數組,我感覺對於test個數比較少的情況下可以不用數組,個人覺得這樣增加了複雜度,並且增加了程序運行時所佔用的空間,(網上的方法大多數將所有的字符utf-8碼(可以想象漢字的個數麼)作爲數組下標移動位數作爲數組內容,因此要分配很大的空間存儲數據,並且查找起來也要話不少時間。)所以我採用了我自己的想法:直接在test中尋找移動位數。並且網上提供的方法,對於壞字的檢測不是很準確(大神們勿噴哈。。。。),這是因爲存儲方式導致的,如果在test中出現了相同的字符,返回值很有可能是負值。可能有的人會對我說的不太懂,舉個例子吧:

在使用壞字檢測時:壞字是b,按我們說的應該往後移動兩個,使得b和test中從後往前的第二個b對齊(在這裏只討論壞字不用好後綴)。但是如果將字符作爲下標存放字符所在位置因爲後面還有b那麼就會將原來的b覆蓋掉,調用壞字檢測時出來的結果反而往前移動了3個。。。。。
ps:如果對於我後面加的不理解,可以去網上找找。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章