KMP 算法是一个高效的字符串匹配算法,由Knuth、Morris、Pratt三人提出,并使用三人名字的首字母命名。在KMP之前,字符串匹配算法往往是遍历字符串的每一个字符进行比对,算法复杂度是O(mn)。而KMP算法通过预处理能够把复杂度降低到O(m+n)。
KMP算法
假设给定一个字符串 1 ABCABCABDEF,现在需要搜索字符串 2 ABCABD 在字符串 1 中出现的位置。从 0 位置开始比对,到位置 5 时发现字符不相同,字符串 1 的字符为 C,字符串 2 的字符为 D。接下来如何继续比对呢?KMP 算法的核心就在于这里。考虑到字符串 2 中有两个 AB 重复出现,因此可以把第一个 AB 移动到第二个 AB 处继续比对。这个移动过程就是利用了已经匹配的字符信息,直接将字符串前移 3 位提高了比对效率。
假设给定一个长度为 n 的字符串 O ,查找长度为 m 的字符串 f 在 O中出现的位置,如下图所示(图片来自网络)。
当比对到第 i 个字符不相等时,需要把字符串 f 向前移动继续比对,KMP算法通过计算最大公共长度来移动。当满足如下条件时,f 可以前移 k 位继续比对。
字符串 A 是 f 的一个前缀;
字符串 B 是 f 的一个后缀;
字符串 A 和字符串 B 相等。
KMP 算法的核心即在于求解 f 中每一个位置之前的字符串的前缀和后缀的最大公共长度。注意,这个最大长度不包括字符串本身。当比较到第 i 位置时,如果不相同,而此时最大公共长度为 j,则 f 前移的距离为 k = i – j 。
最大公共长度的计算
前缀是除了最后一个字符的子字符串,后缀是指除了第一个字符的子字符串。对于字符串 ABCA,前缀有 A、AB、ABC,后缀有 A、CA、BCA,相同的前缀后缀只有 A。最大公共长度是指当前位置前面的字符串相同前缀后缀的最大长度,使用 next 数组表示。根据这个规则,前面的字符串 2 ABCABD 的 next 数组如下表格所示。对于长度为 m 的字符串,next 数组的长度为 m + 1。显而易见,next[0] = next[1] = 0。
已知A3 = A0,next[4] = 1,计算 next[5] 时只需要比对 B4 = B1 则 A3B4 = A0B1,next[5] = next[4] + 1 = 2。
已知 A3B4 = A0B1,next[5] = 2,计算 next[6] 时首先比较 D5 ≠ C2 则 A3B4D5 ≠ A0B1C2。如果存在一个以 D 结尾的公共前缀,那么这个前缀一定在 C2 的前面,这个前缀的长度为 next[next[5]] = 0,因此 C2 的前面没有这样的前缀,next[6] = 0。
假设我们现在已经求得 next[1]、next[2]、……、next[i],现在要求next[i+1]。可以看出,如果位置 i 和位置 next[i] 处的两个字符相同,则 next[i+1] = next[i] + 1;如果两个位置的字符不相同,可以继续向前搜索,获得其最大公共长度 next[next[i]],然后再和位置 i 的字符比较,直到能最终确定结果。(图片来自网络)
根据上面的分析,不难写出求解 next 数组的代码如下。
publicstaticint[]getNext(Stringfind){int[]next=newint[find.length()+1];//0 和 1 的值肯定是 0next[0]=0;next[1]=0;//根据 next[i] 推算 next[i+1]for(inti=1;i&&(find.charAt(i)!=find.charAt(j))){j=next[j];}//如果相等,则 j 加一即可if(find.charAt(i)==find.charAt(j)){j++;}next[i+1]=j;}returnnext;
}
字符串匹配
字符串匹配的过程和求解 next 数组的过程类似。假设搜索字符串 f 在字符串 O 中出现的位置,f 的下标为 j,O 的下标为 i。如果 O(i) = f(j),则 j = j + 1,继续比较 f 中的下一个字符;如果 O(i) ≠ f(j),则 j = next[j],继续比较 f 中位置为 next[j] 的字符,相当于 f 前移了 i - j 位。当 f 的下标 j 移动到末尾时,说明匹配成功,此时可以算出第一次出现的位置是 i - j。
如下图所示,假设 O 为ABCABCABDEF,f 为ABCABD。O(4) = f(4),下一步继续比对 O(5) 和 f(5) 即可;O(5) ≠ f(5),则 f 的下一个比对字符位置为 next[5] = 2,下一步继续比对 O(5) 和 f(2),相当于 f 前移了 5 - 2 = 3 位。当比对到 O(8) = f(5) 时,f 已经比对到末尾,匹配成功,此时算出 O 中出现的位置为 8 - 5 = 3。
根据上面的分析,不难写出匹配字符串的代码如下。
publicstaticListindexOf(Stringtext,Stringfind){int[]next=getNext(find);Listindex=newLinkedList();//i 表示 text 中的位置,j 表示 find 中的位置intj=0;//遍历 text 中的字符for(inti=0;i&&(text.charAt(i)!=find.charAt(j))){j=next[j];}//如果 i 位置和 j 位置的字符相同,移动一位if(text.charAt(i)==find.charAt(j)){j++;}//如果 j 已经到了 find 的尾部,表示已经找到if(j==find.length()){// i - j + 1 即为 find 在 text 中出现的位置index.add(i-j+1);//这里是 KMP 算法的关键点,移动位置为 next[j]j=next[j];}}returnindex;
}
领取专属 10元无门槛券
私享最新 技术干货