一文讀懂kmp算法

自己創建了個博客網站,hofe’s blog ,歡迎大家收藏_
博客將會兩邊同步更新,體驗會更佳。

一、KMP算法是什麼?

kmp算法是用於解決字符串匹配的算法
本文用約定用 pat 表示模式串,長度爲 Mtxt 表示文本串,長度爲 N。KMP 算法是在 txt 中查找子串 pat,如果存在,返回這個子串的起始索引,否則返回 -1

首先來看一道例題:link

題目描述

字符串旋轉:

給定兩字符串A和B,如果能將A從中間某個位置分割爲左右兩部分字符串(都不爲空串),並將左邊的字符串移動到右邊字符串後面組成新的字符串可以變爲字符串B時返回true。

例如:如果A=‘youzan’,B=‘zanyou’,A按‘you’‘zan’切割換位後得到‘zanyou’和B相同返回true。

輸入描述:

2個不爲空的字符串(說明:輸入一個字符串以英文分號";"分割爲2個字符串)
例如:youzan;zanyou 即爲A=‘youzan’,B=‘zanyou’
輸出描述:
輸出true或false(表示是否能按要求匹配兩個字符串)

示例1

輸入
youzan;zanyou
輸出
true

二、爲什麼要用KMP算法?

我們知道從字符串中尋找子串的問題一般可以用暴力遍歷來解決,每一次只能往後移動一個位置,且遇到不匹配的字符時,指針需要回溯,時間複雜度爲O(n*m)

int search(String pat, String txt) {
    int M = pat.length;
    int N = txt.length;
    for (int i = 0; i < N - M; i++) {
        int j;
        for (j = 0; j < M; j++) {
            if (pat[j] != txt[i+j])
                break;
        }
        // pat 全都匹配了
        if (j == M) return i;
    }
    // txt 中不存在 pat 子串
    return -1;
}

KMP算法具有兩個特性:

  1. 僅僅後移模式串
  2. 指針不回溯

什麼意思呢?請看以下兩種情況下的kmp算法

(1)txt = “aaaaaaab” pat = “aaab”

(2) txt = “aaacaaab” pat = “aaab”:

到此爲止大家應該已經理解爲什麼要用kmp算法代替暴力遍歷找子串了

三、KMP怎麼解決問題?

現在來看看kmp如何實現

首先我們可以知道顯著的區別是遇到失配的情況主串不必再回退到當前的下一個字符開始匹配,而是保持不變,不進行回溯。由子串進行回溯重新匹配,而且回退之後,回退點之前的元素需要和主串匹配才行,這樣主串纔不用回退。那關鍵就在於子串回退的位置,它該回退多少的問題。

要保證一個模式串進行移動j位之後,回退點之前的元素仍然和主串匹配,說明模式串的(真)前後綴有一段是相同的。

步驟1:解決回退幾位的問題

這裏引入next[j]表示失配點j,對於字符串aaab它有以下幾種情況(這裏表示的都是真前後綴)

next[0]表示失配點在pat[0]=a這個位置,也就是它前面的元素爲"",沒有前綴與後綴,令next[0] = -1;

next[1]表示失配點在pat[1]=a這個位置,也就是它前面的元素爲a,沒有前綴與後綴,故next[0] = 0;

next[2]表示失配點在pat[2]=a這個位置,也就是它前面的元素爲aa,有前綴與後綴a,故next[1] = 1;

next[3]表示失配點在pat[3]=b這個位置,也就是它前面的元素爲aaa,有前綴與後綴aa,故next[2] = 2;

那就可以得到這串子串的部分匹配表

i 0 1 2 3
pat[] a a a b
next[] -1 0 1 2

那現在用這個匹配表驗證前面兩種主串

(1)txt = “aaaaaaab” pat = “aaab”

失配點 j = 3, pat[j] = b, next[j] = 2;也就是說j回退到2的位置,往回走1位, 即pat[2] = a

(2)txt = “aaacaaab” pat = “aaab”:

圖片是經過優化算法得出的步驟,因爲知道子串中未出現過c,所以可以直接回退到起點,但算法優化前是按照下面流程走的:

第一次: 失配點 j = 3, pat[j] = b, next[j] = 2;j回退到2的位置, 即pat[2] = a

第二次: 失配點 j = 2, pat[j] = a, next[j] = 1;j回退到1的位置, 即pat[1] = a

第三次: 失配點 j = 1, pat[j] = a, next[j] = 0;j回退到next[j]的位置, 即pat[0] = a

第四次: 失配點 j = 0, pat[j] = a, next[j] = -1; j回退到next[j]的位置, 即pat[-1] = “”

代碼如下:

public static int kmp(char[] txt, char[] pat){
        int[] next = getNext(pat);
        int i = 0, j = 0;
        while (i < txt.length && j < pat.length) {
            if (txt[i] == pat[j]) {
                i++;
                j++;
            } else if (next[j] != -1) {
                j = next[j];
            } else {
                // next[j] = -1說明這時已經在頭部位置之前了
                i++;
            }
        }
        return j == pat.length ? i-j : -1;
    }
步驟2:解決求next數組的問題

使用雙指針遍歷該位置前的串中前後綴相同的值,可令next[0] = -1,代表無匹配,回退到該字符串前一位置;

令next[1] = 0,真前後綴是不包含自身的。

代碼如下:

public class static int[] getNext(char[] pat){
        int[] next = new int[arr.length];
        next[0] = -1;
        next[1] = 0;
        int i = 2;
        int j = 0;
        while(i < pat.length){
            if(pat[i-1] == pat[j]){
                next[i] = j + 1;
                i++;
                j++;
            }else if(j > 0){
                // 首尾匹配,但次前綴和次後綴不匹配;
                // 那該位置的值就等於子串次前綴位置的值
                j = next[j];
            }else{
                // 首尾不匹配
                next[i] = 0;
                i++;
            }
        }
        return next;
    }

完整代碼:

import java.util.*;

public class Main{
    
    public static int kmp(char[] txt, char[] pat){
        int[] next = getNext(pat);
        int i = 0, j = 0;
        while (i < txt.length && j < pat.length) {
            if (txt[i] == pat[j]) {
                i++;
                j++;
            } else if (next[j] != -1) {
                j = next[j];
            } else {
                // next[j] = -1說明這時已經在頭部位置之前了
                i++;
            }
        }
        return j == pat.length ? i-j : -1;
    }
    public static int[] getNext(char[] pat){
        int[] next = new int[pat.length];
        next[0] = -1;
        next[1] = 0;
        int i = 2;
        int j = 0;
        while(i < pat.length){
            if(pat[i-1] == pat[j]){
                next[i] = j + 1;
                i++;
                j++;
            }else if(j > 0){
                // 首尾匹配,但次前綴和次後綴不匹配;
                // 那該位置的值就等於子串次前綴位置的值
                j = next[j];
            }else{
                // 首尾不匹配
                next[i] = 0;
                i++;
            }
        }
        return next;
    }
    
    public static void main(String[] args){
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        String s1 = str.split(";")[0];
        String s2 = str.split(";")[1];
        if (s1.length() != s2.length()) {
            System.out.println("false");
            return;
        }
        // 轉化爲判斷txt是否包含sub
        String txt = s1 + s1;
        String sub = s2;
        System.out.println(
            kmp(txt.toCharArray(), sub.toCharArray())==-1?false:true);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章