什么是串
數據結構中,字符串要單獨用一種存儲結構來存儲,稱為串存儲結構。這里的串指的就是字符串。字符串通常是由零個或多個字符組成的有限序列。
一般地,由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算法
朴素模式匹配算法,其實現過程沒有任何技巧,就是簡單粗暴地拿一個串同另一個串中的字符一一比對,得到最終結果。
代碼實現
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
,在暴力解法中,經過某次匹配后,如下:
此時S[9]{A} != P[6]{D}
,暴力算法是令i=i-j+1,j=0
,通過回溯的方式重新匹配,但是i
之前的都是已經比較過的,所以如果能保持i
不變,j
變為2,子串右移4位,即s[9]{A}
和j[2]{C}
對齊,如下
(注:上圖中的黃色框其實就是暴力算法需要比較的,但實際上卻是多余的)
那么我們如何得到這個4位呢?也就是說,我們是怎么知道j
要指向2呢?這就需要用到最大公共子串長度。
在第一張圖示中,當S[9]
和P[6]
匹配失衡時:
- 粉色框中的是匹配的,即
ABCDAB
- 綠色框和藍色框匹配,即
AB
- 藍色框
AB
是粉色框中ABCDAB
的后綴 - 紅色框
AB
是粉色框中ABCDAB
的前綴 - 而藍色框和紅色框都是
AB
,說明對於子串中的粉色框ABCDAB
有相等的前后綴,由2可知,綠配藍,則紅色框也和綠色框匹配 - 所以,將紅色框和綠色框對齊,指針
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]
無法求出)。
如字符串 A,P[0] = A,其左側沒有其他的元素,所以也就不存在前后綴長度了
以子串ABCDABD
為例來構建next數組:
注:上表中的真前后綴是左邊子串的真前后綴
根據上表,我們可得next數組:
由上可知,對於存在最長公共前后綴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]
位。
代碼實現
因為當S[i] != P[j]
,說明了之前的j-1
個字符都匹配上了,則對P
的子串求出最大公共子串長度即可。又得知next[j] = 第j位字符前面j-1位字符組成的子串的前后綴重合字符數 + 1
首先定義一個j
,從左向右遍歷子串P,j
的位置表示P
的子串的后綴的最右字符,再定義一個k
,k
的位置用來表示P的子串的前綴的最右字符
-
已知
next[j]=k
,則P[0~k-1]=P[j-k~j-1]
,前面k-1
位字符和后面的k-1
位字符重合。如當j=0
時,P[0]
沒有最長前后綴,即next[0]=-1
,j=0,k=-1+1=0
,同時j,k
右移進入下一輪循環 -
我們已經求出
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
,如下圖
-
若
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個元素的最大重復子串,如下分析
- 若
分析:
-
給定一個數組如下,假設我們要求
next[16]
的值,已知next[j]=k,next[j+1]=k+1
-
要求
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]
-
如果
P[7]=P[15]
,則next[16]=next[15]+1=8
,就會是下面這種情況,即:next[j+1]=k+1 next[15+1]=7+1 next[16]=8
-
如果
P[7]!=P[15]
,設next[7]=3
,則說明P[0~6]
的最大公共子串長度為3,即P[0~2]=P[4~6]
,由2可知P[0~6]=P[8~14]
,所以以下面藍色部分是重合的而
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數組匹配字符串
- 當
i=j=0
時,若S[i]==P[j]
,字符匹配,i++,j++ - 若
j=-1
,P串需從頭匹配,i++,j++ 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);
}
}