用Java【KMP算法】解決字符串匹配問題

一、應用場景-字符串匹配

在字符串String basicString = "張三王五李四張三 王五李四 王 五李四 ";中查詢
String searchString = "張三 王";,存在則返回首次出現的位置,否則返回-1

二、暴力算法

思路

  • 如果當前字符匹配成功(即basicString[i] == searchString [j]),則i++,j++,繼續匹配下一個字符
  • 如果失配(即basicString[i]! = searchString [j]),令i = i - (j - 1),j = 0。相當於每次匹配失敗時,i 回溯,j 被置爲0。
  • 用暴力方法解決的話就會有大量的回溯,每次只移動一位,若是不匹配,移動到下一位接着判斷,浪費了大量的時間。

代碼實現

public class ViolenceMatch {

    public static void main(String[] args) {
        String basicString = "張三王五李四張三 王五李四 王 五李四 ";
        String searchString = "張三 王";
        int index = violenceMatch(basicString,searchString);
        System.out.println("index:"+index);
    }

    /**
     * 暴力匹配算法
     * @param basicString 基礎字符串
     * @param searchString 待查找的字符串
     * @return
     */
    public static int violenceMatch(String basicString, String searchString){
        char[] basicChar = basicString.toCharArray();
        char[] searchChar = searchString.toCharArray();

        int basicLength = basicChar.length;
        int searchLength = searchChar.length;

        //i索引指向basicChar
        int basicIndex = 0;
        //j索引指向searchChar
        int searchIndex = 0;

        //保證下標不越界
        while(basicIndex < basicLength && searchIndex < searchLength){

            //進行字符匹配
            if(basicChar[basicIndex] == searchChar[searchIndex]){
                basicIndex++;
                searchIndex++;
            }else{
                //字符匹配失敗
                basicIndex = basicIndex - (searchIndex - 1);
                searchIndex = 0;
            }
        }
        //判斷是否匹配成功
        if(searchIndex == searchLength){
            return basicIndex - searchIndex;
        }else{
            return -1;
        }
    }
}

二、KMP算法

1. 介紹

  • KMP是一個解決模式串在文本串是否出現過,如果出現過,最早出現的位置的經典算法
  • Knuth-Morris-Pratt 字符串查找算法,簡稱爲 “KMP算法”,常用於在一個文本串S內查找一個模式串P 的出現位置,這個算法由Donald Knuth、Vaughan Pratt、James H. Morris三人於1977年聯合發表,故取這3人的姓氏命名此算法.
  • KMP方法算法就利用之前判斷過信息,通過一個next數組,保存模式串中前後最長公共子序列的長度,每次回溯時,通過next數組找到,前面匹配過的位置,省去了大量的計算時間

2. 思路分析

舉例來說,有一個字符串 Str1 = “BBC ABCDAB ABCDABCDABDE”,判斷,裏面是否包含另一個字符串 Str2 = “ABCDABD”?

  1. 首先,用Str1的第一個字符和Str2的第一個字符去比較,不符合,關鍵詞向後移動一位
    在這裏插入圖片描述

  2. 重複第一步,還是不符合,再後移。
    在這裏插入圖片描述

  3. 一直重複,直到Str1有一個字符與Str2的第一個字符符合爲止
    在這裏插入圖片描述

  4. 接着比較字符串和搜索詞的下一個字符,還是符合。
    在這裏插入圖片描述

  5. 遇到Str1有一個字符與Str2對應的字符不符合。
    在這裏插入圖片描述

  6. 這時候,想到的是繼續遍歷Str1的下一個字符,重複第1步。(其實是很不明智的,因爲此時BCD已經比較過了,沒有必要再做重複的工作,一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是”ABCDAB”。KMP 算法的想法是,設法利用這個已知信息,不要把”搜索位置”移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。)
    在這裏插入圖片描述

  7. 怎麼做到把剛剛重複的步驟省略掉?可以對Str2計算出一張《部分匹配表》,這張表的產生在後面介紹
    在這裏插入圖片描述

  8. 已知空格與D不匹配時,前面六個字符”ABCDAB”是匹配的。查表可知,最後一個匹配字符B對應的”部分匹配值”爲2,因此按照下面的公式算出向後移動的位數:
    移動位數 = 已匹配的字符數 - 對應的部分匹配值。
    因爲 6 - 2 等於4,所以將搜索詞向後移動 4 位。

  9. 因爲空格與C不匹配,搜索詞還要繼續往後移。這時,已匹配的字符數爲2(”AB”),對應的”部分匹配值”爲0。所以,移動位數 = 2 - 0,結果爲 2,於是將搜索詞向後移 2 位。
    在這裏插入圖片描述

  10. 因爲空格與A不匹配,繼續後移一位。
    在這裏插入圖片描述

  11. 逐位比較,直到發現C與D不匹配。於是,移動位數 = 6 - 2,繼續將搜索詞向後移動 4 位。
    在這裏插入圖片描述

  12. 逐位比較,直到搜索詞的最後一位,發現完全匹配,於是搜索完成。如果還要繼續搜索(即找出全部匹配),移動位數 = 7 - 0,再將搜索詞向後移動 7 位,這裏就不再重複了。
    在這裏插入圖片描述

  13. 介紹《部分匹配表》怎麼產生的
    先介紹前綴,後綴是什麼
    在這裏插入圖片描述
    “部分匹配值”就是”前綴”和”後綴”的最長的共有元素的長度。以”ABCDABD”爲例,
    -”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。

  14. ”部分匹配”的實質是,有時候,字符串頭部和尾部會有重複。比如,”ABCDAB”之中有兩個”AB”,那麼它的”部分匹配值”就是2(”AB”的長度)。搜索詞移動的時候,第一個”AB”向後移動 4 位(字符串長度-部分匹配值),就可以來到第二個”AB”的位置。
    在這裏插入圖片描述

到此KMP算法思想分析完畢!
參考原文詳解

3. 代碼實現

import java.util.Arrays;

public class KMPAlgorithm {

    public static void main(String[] args) {
        String basicString = "張三王五李四張三 王五張三 王 五李四 ";
        String searchString = "張三 王五張三";
        int index = kmpSearch(basicString,searchString);
        System.out.println("index:"+index);
    }

    /**
     * kmp搜索算法
     *
     * @param basicString  源字符串
     * @param searchString 待搜索字符串
     * @return 返回第一個匹配的位置 未找到則返回-1
     */
    public static int kmpSearch(String basicString, String searchString) {
        //獲取待搜索字符串的部分匹配值
        int[] next = kmpNext(searchString);
        //輸出部分匹配表
        System.out.println(Arrays.toString(next));
        for(int i = 0,j = 0;i<basicString.length();i++){
            //需要處理basicString.charAt(i) != searchString.charAt(j)
            //去調整j的大小
            while (j > 0 && basicString.charAt(i) != searchString.charAt(j)){
                j = next[j - 1];
            }
            if(basicString.charAt(i) == searchString.charAt(j)){
                j++;
            }
            if(j == searchString.length()){
                return i - j + 1;
            }
        }

        return -1;
    }

    /**
     * 計算字符串的部分匹配值
     * @param searchString
     * @return
     */
    public static int[] kmpNext(String searchString) {
        //數組保存部分匹配值
        int[] next = new int[searchString.length()];
        //若字符串的長度爲1,部分匹配值爲0
        next[0] = 0;
        //i => 字符,j => 部分匹配值
        for (int i = 1, j = 0; i < searchString.length(); i++) {
            //當searchString.charAt(i) != searchString.charAt(j),需要從next[j-1]獲取新的j
            //知道發現searchString.charAt(i) == searchString.charAt(j) 時退出

            while(j > 0 && searchString.charAt(i) != searchString.charAt(j)){
                j = next[j - 1];
            }

            //部分匹配值+1
            if(searchString.charAt(i) == searchString.charAt(j)){
                j++;
            }
            next[i] = j;
        }
        return next;
    }
}

運行效果
在這裏插入圖片描述
KMP算法參考原文

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