kmp演算法是為了解決傳統字串匹配問題而誕生的
有以下兩個字串A和B,長度分別為 N 和 M,判斷B是否是A的子串,以及開始匹配的位置,不匹配就返回-1
A:”ababababca”
B:”abababca”
傳統的方法就是主串和目標串對位比較,不對則主串重新從下一位開始,時間複雜度接近 O( N × M )
而kmp演算法精妙之處在於透過找尋目標串 B 每一位的資訊,利用這個資訊,使得在字元不匹配的情況下,不需要整體重新來過,只需要移動一定的位置,然後繼續匹配即可。
而這個資訊就是——字首與字尾相等的最大長度
下面介紹一下kmp到底如何加速字串匹配的!
- 首先生成字串B的
next
陣列,而這個陣列的含義是:next[i]
表示next[0....i-1]
上字首與字尾相等的最大長度,舉個例子:”abababca”這個字串中 字元 ‘c’ 的字首和字尾相同的有 字首”abab” 和 字尾 “abab”是最大的,故 字元”c” 的next陣列位置就是 4
下圖展示了”abababca”的next陣列:
- 生成了這個next陣列之後,我們就可以加速主串和目標串的匹配過程了
對於主串”ababababca”,當目標串”abababca”匹配到下標為 j 時,發現'a' != 'c'
,此時主串並沒有重新從i = 1開始匹配,而是讓目標串的 j 開始跳,跳到next[j]
的下標,即j = 4
,即從 j = 4開始與i繼續匹配!如下圖所示
那這樣做的原理是什麼呢
如下圖
首先當主串和目標串匹配到A 和 B 字元的時候發生不匹配,我們知道 1 區域等於3 區域,而 3 區域等於2區域,故1區域一定等於2區域,因為next的陣列的定義就是這樣,字首和字尾相等的最大長度,所以這次不需要主串重新回到 i + 1 的 位置,只需要目標串退回到2區域最後位置 + 1 ,直接從目標串的 C字元開始和 A 字元匹配即可
理解了之後,直接上程式碼!
public class Main(){
public static void main(String[] args){
}
public int kmp(String s1, String s2){
if(s1 == null || s2 == null || s2.length() > s1.length() || s2.length() == 0){
return -1;
}
char[] arr1 = s1.toCharArray();
char[] arr2 = s2.toCharArray();
int len1 = arr1.length;
int len2 = arr2.length;
int[] next = getNext(arr2);
int i = 0;
int j = 0;
while(i < len1 && j < len2){
if(arr1[i] == arr2[j]){
i++;
j++;
}else if(next[j] == -1){
i++;
}else{ //加速的過程體現在這,如果兩個字元不相等,j 是跳到 最大字首長度的下一個位置
j = next[j];
}
}
return j == len2 ? i - j : -1;
}
public int[] getNext(char[] arr){
if(arr.length == 1){
return new int[]{-1};
}
int len = arr.length;
int[] next = new int[len];
next[0] = -1;
next[1] = 0;
int pos = 0;
int cur = 2;
while(cur < len){
if(arr[pos] == arr[cur - 1]){
next[cur] = pos + 1;
cur++;
pos++;
}else if(next[pos] > 0){
pos = next[pos];
}else{
next[cur] = 0;
cur++;
}
}
return next;
}
}
kmp的應用可以用在判斷一棵樹是否是另一顆樹的子樹!!!時間複雜度控制在O(M + N) M和N為兩個樹的結點數。感覺樹的拓撲結構複雜的情況下用kmp不錯,用遞迴的話接近O(M × N)
public boolean isSubTree(TreeNode A, TreeNode B) {
if(A==null||B==null){
return false;
}
return kmp(preString(A),preString(B)) != -1; //kmp一判斷,完事!
}
public String preString(TreeNode root1) {
//先找到兩棵樹的先序序列
if (root1 == null) {
return "!_";
}
String s = root1.val + "_";
s += preString(root1.left);
s += preString(root1.right);
return s;
}
此題的遞迴解法:
public boolean isSubtree(TreeNode s, TreeNode t) {
if(s == null && t != null){
return false;
}
if(s == null && t == null){
return true;
}
//無非就是,兩個樹相同,t是s的左子樹,t是s的右子樹!這三種情況
return isSameTree(s,t) || isSubtree(s.left,t) || isSubtree(s.right,t);
}
public boolean isSameTree(TreeNode s, TreeNode t){
if(s == null && t == null){
//兩樹都一起到達底部
return true;
}
if(s == null || t == null){
//一個樹有值,另一個樹為空,則一定不為子樹
return false;
}
if(s.val != t.val){
//不相等則更不用說
return false;
}
return isSameTree(s.left,t.left) && isSameTree(s.right,t.right);
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結