BF算法和KMP算法


什么是串

數據結構中,字符串要單獨用一種存儲結構來存儲,稱為串存儲結構。這里的串指的就是字符串。字符串通常是由零個或多個字符組成的有限序列。

一般地,由n個字符串構成的串記作: S="a0a1......an-1"(n≥0),串中的ai(1≤i≤n)

  • n是一個有限的數值
  • 串一般記為S是串的名稱,用雙引號或單引號括起來的字符序列是串的值(引號不屬於串的內容)
  • 可以是字母、數字或其他字符,i就是該字符在串中的位置。串中的字符數目n稱為串的長度,n是一個有限的數值

無論學習哪種編程語言,操作最多的總是字符串。數據結構中,根據串中存儲字符的數量及特點,對一些特殊的串進行了命名,如下:

  • 空串:存儲 0 個字符的串,例如 S = ""(雙引號緊挨着)

  • 空格串:只包含空格字符的串,例如 S = " "(雙引號包含 5 個空格)

  • 主串和子串:假設有兩個串 A 和 B,如果 B 中可以找到幾個連續字符組成的串與 A 完全相同,則稱 A 是 B 的主串,B 是 A 的子串。例如,若 A = "ZIHUCHUAN",B = "HUA",由於 A 中也包含 "HUA",因此串 A 和串 B 是主串和子串的關系

  • 前綴(prefix)、真前綴(proper prefix)、后綴(suffix)、真后綴(proper suffix),真前(后)綴就是指不包含自身的前(后)

    如給定一個字符串string,則:

判斷兩個串之間是否具有主串和子串的關系,主要匹配算法有以下兩種:

  • 朴素模式匹配算法(Brute-Force,BF算法),也叫暴力算法
  • 快速模式匹配算法(Knuth-Morris-Pratt,KMP算法)

BF算法

朴素模式匹配算法,其實現過程沒有任何技巧,就是簡單粗暴地拿一個串同另一個串中的字符一一比對,得到最終結果。

image-20210811000042488

代碼實現

public class BruteForce {

    /**
     * @param s 主串
     * @param p 子串
     */
    public static int bruteForce(String s, String p) {
        //匹配初始位置
        int sl = s.length();
        int pl = p.length();
        if (sl < pl) return -1;
        int i = 0, j = 0;
        while (i < sl && j < pl) {
            if (s.charAt(i) == p.charAt(j)) {
                i++;
                j++;
            } else {//回溯
                i = i - j + 1;
                j = 0;
            }
        }
        if (j >= pl) {//說明已經匹配完成
            return i - j;
        } else {//未匹配到
            return -1;
        }
    }

    public static void main(String[] args) {
        String s = "ZIHUCHUAN";
        String p = "HUA";
        System.out.println(bruteForce(s, p));
    }
}

KMP算法

KMP算法,是一個效率非常高的字符串匹配算法,其核心思想就是主串不回溯,模式串盡量多地往右移動。

具體實現就是通過一個next數組實現,next[k]表示的是前k的字符組成的這個子串最大公共子串長度

最大公共子串長度

對於一個字符串來說,它既有前綴,又有后綴,真前(后)綴就是指不包含自身的前(后)綴。

這里的最大公共子串長度,就是指該字符串最長的且相等的真前綴和真后綴。如:

abcd
真前綴: a,ab,abc       前綴:a,ab,abc,abcd
真后綴: bcd,cd,d       后綴: abcd,bcd,cd,d

很明顯可以看出,上面的字符串abcd沒有公共前后綴,也就不存在最長公共前后綴了,而對於字符串abcab,如下:

abcab
真前綴: a,ab,abc,abca   前綴: a,ab,abc,abca,abcab
真后綴: bcab,cab,ab,b   后綴: abcab,bcab,cab,ab,b

其中真前綴ab和真后綴ab相等且唯一,即字符串abcab的最大公共子串長度為ab,其長度為2。

很明顯,從上面的分析可以得出真前綴包含在前綴里面,所以后面涉及到的前(后)綴都表示真前(后)綴

構建next數組

next數組的作用是什么?

用來存放最大公共子串的長度,這個長度也就是當主串和子串不匹配的時候,子串需要回退的位置。

最大公共子串的長度作用是什么?

如果S[i] != P[j],也就是第一次不匹配,這說明了之前的j-1(如果存在)個字符都匹配上了,對於這j-1個字符構成的字符串P(j-1),也就是P的子串,我們只需要從P的子串的最大公共子串處開始下一輪比較即可,主串不需要再回退。

若給定一個主串BBCABCDABABCDABCDABDE和子串ABCDABD,在暴力解法中,經過某次匹配后,如下:

image-20210811234948723

此時S[9]{A} != P[6]{D},暴力算法是令i=i-j+1,j=0,通過回溯的方式重新匹配,但是i之前的都是已經比較過的,所以如果能保持i不變,j變為2,子串右移4位,即s[9]{A}j[2]{C}對齊,如下

image-20210812170703163

(注:上圖中的黃色框其實就是暴力算法需要比較的,但實際上卻是多余的)

那么我們如何得到這個4位呢?也就是說,我們是怎么知道j要指向2呢?這就需要用到最大公共子串長度。

在第一張圖示中,當S[9]P[6]匹配失衡時:

  1. 粉色框中的是匹配的,即ABCDAB
  2. 綠色框和藍色框匹配,即AB
  3. 藍色框AB是粉色框中ABCDAB的后綴
  4. 紅色框AB是粉色框中ABCDAB的前綴
  5. 而藍色框和紅色框都是AB,說明對於子串中的粉色框ABCDAB有相等的前后綴,由2可知,綠配藍,則紅色框也和綠色框匹配
  6. 所以,將紅色框和綠色框對齊,指針j指向紅色框的后一位,從而保持i不移動,而j移到位置2,且最大公共子串為AB,長度為2

由此可知,當S[i]P[j]匹配失衡時,計算出不包括P[j]的左邊子串(ABCDAB)的最長公共前后綴的長度。假設長度為k,則j指針需要重置為k(如上圖中的j=2),i不變,繼續匹配。

怎么求子串的最大公共長度?

借助next[]數組,當子P串在位置 j 失配的時候,需要將 j 指針重置為next[j],而next[j]就代表了P字符串的子串P[0~j-1]的最長公共前后綴,其中對於next[0]來說,我們一般把他設為-1(因為P[0]的左邊沒有子串,所以next[0]無法求出)。

image-20210813115549214

如字符串 A,P[0] = A,其左側沒有其他的元素,所以也就不存在前后綴長度了

以子串ABCDABD為例來構建next數組

image-20210812151136382

注:上表中的真前后綴是左邊子串的真前后綴

根據上表,我們可得next數組:

image-20210812173258814

由上可知,對於存在最長公共前后綴k,前綴P[0~k-1]和后綴P[j-k~j-1]相等(j>k),則有next[j]=k,說明P[j]之前的子串中有長度為k的前后綴,所以在KMP匹配過程中,當匹配失衡時,只需要將j移動到next[j]的位置繼續匹配,相當於子串P在原來的位置上右移next[j]位。

image-20210813094142864

代碼實現

因為當S[i] != P[j],說明了之前的j-1個字符都匹配上了,則對P的子串求出最大公共子串長度即可。又得知next[j] = 第j位字符前面j-1位字符組成的子串的前后綴重合字符數 + 1

首先定義一個j,從左向右遍歷子串P,j的位置表示P的子串的后綴的最右字符,再定義一個kk的位置用來表示P的子串的前綴的最右字符

  1. 已知next[j]=k,則P[0~k-1]=P[j-k~j-1],前面k-1位字符和后面的k-1位字符重合。如當j=0時,P[0]沒有最長前后綴,即next[0]=-1j=0,k=-1+1=0,同時j,k右移進入下一輪循環

  2. 我們已經求出next[j]=k,下一步應該求next[j+1],這時分以下兩種情況

    • P[j]==P[k],重復的字符串個數會增加,則P[0~k-1]+P[k] = P[j-k~j-1]+P[j],即P[0~k]=P[j-k~j],前面k位字符和后面的k位字符重合,即多了一位,所以可得出next[j+1]=k+1,即next[++j]=++k,如下圖

    image-20210813201437948

    • P[j]!=P[k],說明重復的字符串個數不會增加,也就是最大重復子串的長度不能加。這個時候我們要去求next[j+1]的值,顯然這個時候就是要求j+1位前面的子串,即P[0~j]的最大重復子串長度。我們假設最長重復子串長度為k1,則

      P[j]==P[k]時,最大重復子串長度=k,P[0~k-1]=P[j-k~j-1]
      P[j]!=P[k]時,最大重復子串長度=k1,則P[0~k1-1]=P[j+1-k~j]
      

      因為此時最大長度不在增加,所以k1 <= k,也就是說現在的最大重復子串可能存在於P[0~k-1],也就是next[k]的值表示前k個元素的最大重復子串,如下分析

分析

  1. 給定一個數組如下,假設我們要求next[16]的值,已知next[j]=k,next[j+1]=k+1

    image-20210813232331610

  2. 要求next[16]的值,即next[j+1]的值,必然next[15]的值是已知的,我們假設next[15]=7,即j=15,k=7,說明P[0~k-1]=P[j-k~j-1]的最大公共子串長度為k=7,則P[0~6]=P[8~14]

    image-20210814132612089

    • 如果P[7]=P[15],則next[16]=next[15]+1=8,就會是下面這種情況,即:

      next[j+1]=k+1
      next[15+1]=7+1
      next[16]=8
      

      image-20210814132851654

    • 如果P[7]!=P[15],設next[7]=3,則說明P[0~6]的最大公共子串長度為3,即P[0~2]=P[4~6],由2可知P[0~6]=P[8~14],所以以下面藍色部分是重合的

      image-20210814153508423

      next[7]的值表示的是P[0~k-1]的最大公共子串長度,所以很明顯P[k]p[next[k]]必然不相等,但是A又和B相等,所以當P[7]!=P[15]的時候,把k的位置重置為next[k],也就是k=next[k],這時k=3,所以此時可以保證k和j仍有公共前后綴。然后再去判斷p[next[k]]P[j]是否相等,若相等則P[j+1]=k+1,P[16]=3+1

      依次類推直到next[1]=0,說明這時沒有公共前后綴。

    /**
     * 查找next數組
     */
    public static int[] getNext(String p) {
        int len = p.length();
        //構建next表
        int[] next = new int[len];
        int k = -1;//表示后綴的最后以為,
        int j = 0;//表示前綴的最后一位
        //規定next[0]為-1
        next[0] = -1;
        while (j < len - 1) {//循環p串的前串
            if (k == -1 || p.charAt(j) == p.charAt(k)) {
                next[++j] = ++k;
            } else {
                k = next[k];
            }
        }
        return next;
    }
    

next數組匹配字符串

  1. i=j=0時,若S[i]==P[j],字符匹配,i++,j++
  2. j=-1,P串需從頭匹配,i++,j++
  3. S[i]!=P[j],匹配失衡,j=next[j]

代碼實現

public class KMP {

    public static int kmp(String s, String p) {
        //獲取next表
        int[] next = getNext(p);
        //匹配初始位置
        int i = 0;
        int j = 0;
        while (i < s.length() && j < p.length()) {
            if (j == -1 || s.charAt(i) == p.charAt(j)) {
                i++;
                j++;
            } else {
                j = next[j];
            }
        }
        if (j >= p.length()) {
            return i - j;
        }
        return -1;
    }


    /**
     * 查找next數組
     */
    public static int[] getNext(String p) {
        int len = p.length();
        //構建next表
        int[] next = new int[len];
        int k = -1;//表示后綴的最后以為,
        int j = 0;//表示前綴的最后一位
        //規定next[0]為-1
        next[0] = -1;
        while (j < len - 1) {//循環p串的前串
            if (k == -1 || p.charAt(j) == p.charAt(k)) {
                next[++j] = ++k;
            } else {
                k = next[k];
            }
        }
        return next;
    }


    public static void main(String[] args) {
        int kmp = kmp("BBCABCDABABCDABCDABDE", "ABCDABD");
        System.out.println(kmp);
    }
}

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM