一、應用場景-字符串匹配
在字符串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”?
-
首先,用Str1的第一個字符和Str2的第一個字符去比較,不符合,關鍵詞向後移動一位
-
重複第一步,還是不符合,再後移。
-
一直重複,直到Str1有一個字符與Str2的第一個字符符合爲止
-
接着比較字符串和搜索詞的下一個字符,還是符合。
-
遇到Str1有一個字符與Str2對應的字符不符合。
-
這時候,想到的是繼續遍歷Str1的下一個字符,重複第1步。(其實是很不明智的,因爲此時BCD已經比較過了,沒有必要再做重複的工作,一個基本事實是,當空格與D不匹配時,你其實知道前面六個字符是”ABCDAB”。KMP 算法的想法是,設法利用這個已知信息,不要把”搜索位置”移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。)
-
怎麼做到把剛剛重複的步驟省略掉?可以對Str2計算出一張《部分匹配表》,這張表的產生在後面介紹
-
已知空格與D不匹配時,前面六個字符”ABCDAB”是匹配的。查表可知,最後一個匹配字符B對應的”部分匹配值”爲2,因此按照下面的公式算出向後移動的位數:
移動位數 = 已匹配的字符數 - 對應的部分匹配值。
因爲 6 - 2 等於4,所以將搜索詞向後移動 4 位。 -
因爲空格與C不匹配,搜索詞還要繼續往後移。這時,已匹配的字符數爲2(”AB”),對應的”部分匹配值”爲0。所以,移動位數 = 2 - 0,結果爲 2,於是將搜索詞向後移 2 位。
-
因爲空格與A不匹配,繼續後移一位。
-
逐位比較,直到發現C與D不匹配。於是,移動位數 = 6 - 2,繼續將搜索詞向後移動 4 位。
-
逐位比較,直到搜索詞的最後一位,發現完全匹配,於是搜索完成。如果還要繼續搜索(即找出全部匹配),移動位數 = 7 - 0,再將搜索詞向後移動 7 位,這裏就不再重複了。
-
介紹《部分匹配表》怎麼產生的
先介紹前綴,後綴是什麼
“部分匹配值”就是”前綴”和”後綴”的最長的共有元素的長度。以”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。 -
”部分匹配”的實質是,有時候,字符串頭部和尾部會有重複。比如,”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算法參考原文