問題
有一個字符串“ABCABABCABDA”,問該字符串裏面是否包含“ABCABD”,是的話請求出下標位置。
思路
一、簡單的想法
最容易想到的方法應該就是讓target的第一個字符和pattern的第一個字符比較,如果不相等,則讓pattern後移一位,直到target有一個字符,與pattern的第一個字符相同爲止。接着比較target和pattern的下一個字符,如果在pattern匹配完之前,target有一個字符與pattern匹配不相等,那麼就讓pattern整個後移一位,再從頭逐個比較。顯然,這樣效率會很差。比如這道題,我們畫個圖來模擬匹配過程。
當匹配到pattern的第6個字符D時,和A不相同。所以就讓pattern後移一位,從第一個字符A與target的第二個字符B比較。可見每次字符不匹配時都要回溯到開始位置的這種做法時間開銷會比較大。
二、KMP算法
那麼又要怎麼做纔能有好的效率呢?
我們看看第一次匹配失敗的情況,前面已經成功匹配了5個字符,我們從圖可以很容易看出,如果將pattern直接移到target的第4個字符而不是隻後移一位,那麼這個時候又已經匹配成功了2個字符,即AB,這樣就提高了效率。如下圖:
那麼,這其中是有什麼聯繫嗎?
我們來看看pattern這個字符串ABCABD,在第一次已經匹配的前5個字符裏,有個特點,就是前後有2個字符是重複的,即AB。所以通過這個前後重複的字符個數,我們就能知道在target裏最有可能匹配pattern的下個新的位置。
KMP算法就是利用了pattern本身的信息提出來的!
(一)生成部分匹配表
首先,我們需要根據pattern來生成一張部分匹配表。先了解概念“前綴”和“後綴”。
“前綴”指除了最後一個字符以外,一個字符串的全部頭部組合。
“後綴”指除了第一個字符以外,一個字符串的全部尾部組合。
所以,“部分匹配值”就是“前綴”和“後綴”的最長共有元素的長度。以本例的pattern(“ABCABD”)爲例加深認識。
- “A”的前綴和後綴都爲空集,共有元素的長度爲0
- “AB”的前綴爲[A],後綴爲[B],共有元素的長度爲0
- “ABC”的前綴爲[A,AB],後綴爲[BC,C],共有元素的長度爲0
- “ABCA”的前綴爲[A,AB,ABC],後綴爲[BCA,CA,A],共有元素爲“A”,長度爲1
- “ABCAB”的前綴爲[A,AB,ABC,ABCA],後綴爲[BCAB,CAB,AB,A],共有元素爲“AB”,長度爲2
“ABCABD”的前綴爲[A,AB,ABC,ABCA,ABCAB],後綴爲[BCABD,CABD,ABD,BD,D],共有元素的長度爲0
所以部分匹配表就是:
那麼這張表該怎麼用程序生成呢?
我們可以用迭代的方式。假設matchTable[n]是部分匹配表(注意數組下標是從0開始的),對於pattern的前i-1個已知部分匹配值的字符,如果匹配值爲count,則對於pattern的第i個字符,有如下可能:
- pattern[i] == pattern[count],此時matchTable[i]=count+1=matchTable[i-1]+1。
- pattern[i] != pattern[count],此時只能在pattern的前count個字符組成的字串中找出最大的且能與pattern[i]匹配的部分匹配值
具體代碼見最後。
(二)KMP算法匹配過程
有了部分匹配表,我們就能根據下面的公式來算出pattern向後移動的位數。
移動位數=已匹配的字符數-對應的部分匹配值
所以完整的匹配過程是:
可見,這樣大大減少了比較次數,提高了效率。
最後,附上具體的實現代碼。
代碼
//
// Created by huxijie on 17-3-13.
// KMP算法的實現
#include <iostream>
#include <string>
using namespace std;
//生成樣式字符串的部分匹配表
int *generateMatchTable(const string& pattern,int *matchTable) {
int length = pattern.length();
if (matchTable == NULL) {
return NULL;
}
int count = 0; //部分匹配值,表示目前“前綴”和“後綴”的最長共有元素的個數,0表示沒有,以此類推
matchTable[0] = 0;
//迭代求pattern前i個字符的部分匹配值
for (int i = 1; i < length; ++i) {
count = matchTable[i - 1];
//對於pattern[i]!=pattern[count]的情況,此時只能在pattern的
//前count個字符組成的字串中找出最大的且能與pattern[i]匹配的部分匹配值
while (count >= 1 && pattern[i] != pattern[count]) {
count = matchTable[count-1]; //注意數組下標是從0開始的
}
//匹配成功,則在上一個部分匹配值的基礎上加1
if (pattern[i] == pattern[count]) {
matchTable[i] = count + 1;
} else { //否則直接置爲0,表示“前綴”和“後綴”沒有共有字符
matchTable[i] = 0;
}
}
return matchTable;
}
//結合部分匹配表進行字符串匹配過程
int kmpMatch(const string& target,const string& pattern) {
//求出部分匹配表
int *matchTable = new int[pattern.length()];
matchTable = generateMatchTable(pattern,matchTable);
const int targetLength = target.length();
const int patternLength = pattern.length();
int targetIndex = 0;
int patternIndex = 0;
while (targetIndex < targetLength && patternIndex < patternLength) {
if (target[targetIndex] == pattern[patternIndex]) {
targetIndex++;
patternIndex++;
} else if (0 == patternIndex) {
targetIndex++;
} else {
patternIndex = matchTable[patternIndex-1];
}
}
delete (matchTable);
if (patternIndex == patternLength) {
return targetIndex - patternIndex;
} else {
return -1;
}
}
int main() {
string target = "ABCABABCABDA";
string pattern = "ABCABD";
cout << kmpMatch(target, pattern);
return 0;
}