1.0 問題描述
實現KMP演算法查詢字串。
2.0 問題分析
- “KMP演算法”是對字串查詢“簡單演算法”的優化。
- 字串查詢“簡單演算法”是源字串每個字元分別使用匹配串進行匹配,一旦失配,模式串下標歸0,源字串下標加1。
- 可以很容易計算字串查詢“簡單演算法”的時間複雜度為O(m*n),其中n表示源字串長度,m表示匹配串長度。
- KMP演算法的匹配方式同簡單演算法的匹配方式相同,只不過在失配的時候,模式串下標不歸零,反而會根據模式串自身的重複資訊,迴歸到一個大於0的下標。從而減少匹配次數。
- 那麼失配時候,模式串下標迴歸的位置需要提前計算好。計算的方法是,匹配串中,每個位置的字首字串“頭部”和“尾部”的重複字元數即為失配下標迴歸位置。
- 假設匹配串是:ababc
- 下標0的字元是“a”,它的字首是空字串,所以迴歸位置為0
- 下標1的字元是“b”,它的字首是“a”,數量不足2個,迴歸位置也是0
- 下標2的字元是“a”,他的字首是“ab”,沒有重複的頭部和尾部,迴歸位置是0
- 下標3的字元是“b”,它的字首是“aba”,有重複的頭部和尾部,重複子串是“a”,長度為1,迴歸位置是1
- 下標3的字元是“c”,它的字首是“abab”,有重複的頭部和尾部,重複子串是“ab”,長度為2,迴歸位置是2
- 根據上一條,我們可以計算出一個next陣列用於儲存失配時模式串下標應該回到哪裡的下標序列。
- 計算好next陣列後,就可以使用“簡單演算法”的邏輯進行匹配了,只不過不同的是,一旦失配,匹配串下標不是迴歸到0,而是根據next陣列決定。
3.0 程式碼實現
3.1使用swift實現
///簡單查詢
func simpleSearch(_ src: String, _ mode: String, _ start: Int) -> Int {
var i = start;
while i < src.count {
var j = 0;
while j < mode.count {
if(i + j >= src.count || src[i + j] != mode[j]){
break;
}
j += 1;
}
if j == mode.count{
return i;
}
i += 1;
}
return -1;
}
///kmp字串查詢
func kmpSearch(_ src: String, _ mode: String, _ start: Int) -> Int {
//計算next
var next: [Int] = [0, 0];
//自身匹配,i表示主下標,j表示匹配下標
var i = 2, j = 0;
while i < mode.count {
if(mode[i - 1] == mode[j]){
next[i] = j + 1;
i += 1;
j += 1;
}else{
if(j == 0){
i += 1;
}
//已經匹配的部分有可能會有首尾相同的情況
j = next[j];
}
}
//演算法同自身匹配
i = start;
j = 0;
while i < src.count {
if(src[i] == mode[j]){
i += 1;
j += 1;
if(j == mode.count){
return i - mode.count;
}
}else{
if(j == 0){
i += 1;
}
j = next[j];
}
}
return -1;
}
複製程式碼
3.2使用js實現
function simpleSearch(src, mode, start){
for(let i = start; i < src.length; i++){
let miss = false;
for(let j = 0; j < mode.length; j++){
if(i + j >= src.length){
miss = true;
break;
}else if(src.charAt(i + j) != mode.charAt(j)){
miss = true;
break;
}
}
if(!miss){
return i;
}
}
return -1;
}
function kmpSearch(src, mode, start){
let next = [0, 0];
let i = 2;
let j = 0;
while (i < mode.length) {
if(mode.charAt(i - 1) == mode.charAt(j)){
j++;
i++;
next[i-1] = j;
}else{
if(j == 0){
i++;
}
j = next[j];
}
}
i = start;
j = 0;
let found = false;
while (i <= src.length) {
if(src.charAt(i) == mode.charAt(j)){
i++;
j++;
if(j == mode.length){
found = true;
break;
}
}else{
if(j == 0){
i++;
}
j = next[j];
}
}
if(found){
return i - mode.length;
}else{
return -1;
}
}
複製程式碼
4.0 複雜度分析
- 我們選取複雜度最大的一種模型來分析,即:模式串所有字元都相同,源串和模式串總是在最後一位失配。
- 令源串為 “aaaabaaaabaaaab”,匹配串為 “aaaaa”。
- 匹配串的next陣列為:[0,0,1,2,3]
- 首次匹配會在第5位失配,比較次數為5。
- 模式串回到3,進行一次比較即會失配,比較次數為1。
- 模式串回到1,進行一次比較即會失配,比較次數為1。
- 模式串回到0,同時源串下標加1。此時源串下標為6,匹配串下標為0。根據源串特點,此時會不斷重複4-7的過程。
- 根據上述分析,我們推到一般情況,長度為n的源串,長度為m的匹配串,會形成一個週期性匹配,週期次數為n/m。
- 很容易看到一個週期內的複雜度小於O(2m),所以整體複雜度小於 O(2m*n/m)=O(2n),即複雜度為O(n)。
- 計算next陣列的演算法和匹配演算法相同,因此複雜度為O(m)。
- 所以KMP演算法整體複雜度為O(m+n)。