注:若是文中視頻不可播放,你們能夠去查看原文,我以爲即便不看視頻應該也能夠讀懂。php
爲保證代碼嚴謹性,文中全部代碼均在 leetcode 刷題網站 AC ,你們能夠放心食用。java
皇上生辰之際,舉國同慶,袁記菜館做爲天下第一飯店,因此被選爲此次慶典的菜品供應方,此次慶典對於袁記菜館是一項史無前例的挑戰,畢竟是第一次給皇上慶祝生辰,稍有不慎就是掉腦殼的大罪,整個袁記菜館內都在緊張的佈置着。此時忽然有一個店小二慌慌張張跑到袁廚面前彙報,到底發生了什麼事,讓店小二如此慌張呢?git
袁記菜館內程序員
店小二:很差了很差了,掌櫃的,出大事了。github
袁廚:發生什麼事了,慢慢說,如此慌張,成何體統。(開店開久了,架子出來了哈)面試
店小二:皇上按照我們菜單點了 666 道菜,可是我們作西湖醋魚的師傅請假回家結婚了,不知道皇上有沒有點這道菜,若是點了這道菜,我們作不出來,那我們店可就完了啊。算法
(袁廚聽了以後,嚇得一屁股坐地上了,緩了半天說道)數組
袁廚:別說那麼多了,快給我找找皇上點的菜裏面,有沒有這道菜!微信
找了好久,而且覈對了不少遍,最後確認皇上沒有點這道菜。菜館內的人都鬆了一口氣markdown
經過上面的一個例子,讓咱們簡單瞭解了字符串匹配。
字符串匹配:設 S 和 T 是給定的兩個串,在主串 S 中找到模式串 T 的過程稱爲字符串匹配,若是在主串 S 中找到 模式串 T ,則稱匹配成功,函數返回 T 在 S 中首次出現的位置,不然匹配不成功,返回 -1。
例:
在上圖中,咱們試圖找到模式 T = baab,在主串 S = abcabaabcabac 中第一次出現的位置,即爲紅色陰影部分, T 第一次在 S 中出現的位置下標爲 4 ( 字符串的首位下標是 0 ),因此返回 4。若是模式串 T 沒有在主串 S 中出現,則返回 -1。
解決上面問題的算法咱們稱之爲字符串匹配算法,今天咱們來介紹三種字符串匹配算法,你們記得打卡呀,說不許面試的時候就問到啦。
這個算法很容易理解,就是咱們將模式串和主串進行比較,一致時則繼續比較下一字符,直到比較完整個模式串。不一致時則將模式串後移一位,從新從模式串的首位開始對比,重複剛纔的步驟下面咱們看下這個方法的動圖解析,看完確定一下就能搞懂啦。
視頻詳解
由於不能夠放置視頻,因此想看視頻的同窗,能夠去看公衆號原文,那裏有視頻
經過上面的代碼是否是一下就將這個算法搞懂啦,下面咱們用這個算法來解決下面這個經典題目吧。
給定一個 haystack 字符串和一個 needle 字符串,在 haystack 字符串中找出 needle 字符串出現的第一個位置 (從0開始)。若是不存在,則返回 -1。
示例 1:
輸入: haystack = "hello", needle = "ll" 輸出: 2
示例 2:
輸入: haystack = "aaaaa", needle = "bba" 輸出: -1
其實這個題目很容易理解,可是咱們須要注意的是一下幾點,好比咱們的模式串爲 0 時,應該返回什麼,咱們的模式串長度大於主串長度時,應該返回什麼,也是咱們須要注意的地方。下面咱們來看一下題目代碼吧。
class Solution {
public int strStr(String haystack, String needle) {
int haylen = haystack.length();
int needlen = needle.length();
//特殊狀況
if (haylen < needlen) {
return -1;
}
if (needlen == 0) {
return 0;
}
//主串
for (int i = 0; i < haylen - needlen + 1; ++i) {
int j;
//模式串
for (j = 0; j < needlen; j++) {
//不符合的狀況,直接跳出,主串指針後移一位
if (haystack.charAt(i+j) != needle.charAt(j)) {
break;
}
}
//匹配成功
if (j == needlen) {
return i;
}
}
return -1;
}
}
複製代碼
咱們看一下BF算法的另外一種算法(顯示回退),其實原理同樣,就是對代碼進行了一下修改,只要是看完我們的動圖,這個也可以一下就能看懂,你們能夠結合下面代碼中的註釋和動圖進行理解。
class Solution {
public int strStr(String haystack, String needle) {
//i表明主串指針,j模式串
int i,j;
//主串長度和模式串長度
int halen = haystack.length();
int nelen = needle.length();
//循環條件,這裏只有 i 增加
for (i = 0 , j = 0; i < halen && j < nelen; ++i) {
//相同時,則移動 j 指針
if (haystack.charAt(i) == needle.charAt(j)) {
++j;
} else {
//不匹配時,將 j 從新指向模式串的頭部,將 i 本次匹配的開始位置的下一字符
i -= j;
j = 0;
}
}
//查詢成功時返回索引,查詢失敗時返回 -1;
int renum = j == nelen ? i - nelen : -1;
return renum;
}
}
複製代碼
咱們剛纔說過了 BF 算法,可是 BF 算法是有缺陷的,好比咱們下面這種狀況
如上圖所示,若是咱們利用 BF 算法,遇到不匹配字符時,每次右移一位模式串,再從新從頭進行匹配,咱們觀察一下,咱們的模式串 abcdex 中每一個字符都不同,可是咱們第一次進行字符串匹配時,abcde 都匹配成功,到 x 時失敗,又由於模式串每位都不相同,因此咱們不須要再每次右移一位,再從新比較,咱們能夠直接跳過某些步驟。以下圖
咱們能夠跳過其中某些步驟,直接到下面這個步驟。那咱們是依據什麼原則呢?
咱們以前的 BF 算法是從前日後進行比較 ,BM 算法是從後往前進行比較,咱們來看一下具體過程,咱們仍是利用上面的例子。
BM 算法是從後往前進行比較,此時咱們發現比較的第一個字符就不匹配,咱們將主串這個字符稱之爲壞字符,也就是 f ,咱們發現壞字符以後,模式串 T 中查找是否含有該字符(f),咱們發現並不存在 f,此時咱們只需將模式串右移到壞字符的後面一位便可。以下圖
那咱們在模式串中找到壞字符該怎麼辦呢?
此時咱們的壞字符爲 f ,咱們在模式串中,查找發現含有壞字符 f,咱們則須要移動模式串 T ,將模式串中的 f 和壞字符對齊。見下圖。
而後咱們繼續從右往左進行比較,發現 d 爲壞字符,則須要將模式串中的 d 和壞字符對齊。
那麼咱們在來思考一下這種狀況,那就是模式串中含有多個壞字符怎麼辦呢?
那麼咱們爲何要讓最靠右的對應元素與壞字符匹配呢?若是上面的例子咱們沒有按照這條規則看下會產生什麼問題。
若是沒有按照咱們上述規則,則會漏掉咱們的真正匹配。咱們的主串中是含有 babac 的,可是卻沒有匹配成功,因此應該遵照最靠右的對應字符與壞字符相對的規則。
咱們上面一共介紹了三種移動狀況,分別是下方的模式串中沒有發現與壞字符對應的字符,發現一個對應字符,發現兩個。這三種狀況咱們分別移動不一樣的位數,那咱們是根據依據什麼來決定移動位數的呢?下面咱們給圖中的字符加上下標。見下圖
下面咱們來考慮一下這種狀況。
此時這種狀況確定是不行的,不往右移動,甚至還有可能左移,那麼咱們有沒有什麼辦法解決這個問題呢?繼續往下看吧。
好後綴其實也很容易理解,咱們以前說過 BM 算法是從右往左進行比較,下面咱們來看下面這個例子。
這裏若是咱們按照壞字符進行移動是不合理的,這時咱們可使用好後綴規則,那麼什麼是好後綴呢?
BM 算法是從右往左進行比較,發現壞字符的時候此時 cac 已經匹配成功,在紅色陰影處發現壞字符。此時已經匹配成功的 cac 則爲咱們的好後綴,此時咱們拿它在模式串中查找,若是找到了另外一個和好後綴相匹配的串,那咱們就將另外一個和好後綴相匹配的串 ,滑到和好後綴對齊的位置。
是否是感受有點拗口,不要緊,咱們看下圖,紅色表明壞字符,綠色表明好後綴
上面那種狀況搞懂了,可是咱們思考一下下面這種狀況
上面咱們說到了,若是在模式串的頭部沒有發現好後綴,發現好後綴的子串也能夠。可是爲何要強調這個頭部呢?
咱們下面來看一下這種狀況
可是當咱們在頭部發現好後綴的子串時,是什麼狀況呢?
下面咱們經過動圖來看一下某一例子的具體的執行過程
視頻
說到這裏,壞字符和好後綴規則就算說完了,壞字符很容易理解,咱們對好後綴總結一下
1.若是模式串含有好後綴,不管是中間仍是頭部能夠按照規則進行移動。若是好後綴在模式串中出現屢次,則以最右側的好後綴爲基準。
2.若是模式串頭部含有好後綴子串則能夠按照規則進行移動,中間部分含有好後綴子串則不能夠。
3.若是在模式串尾部就出現不匹配的狀況,即不存在好後綴時,則根據壞字符進行移動,這裏有的文章沒有提到,是個須要特別注意的地方,我是在這個論文裏找到答案的,感興趣的同窗能夠看下。
Boyer R S,Moore J S. A fast string searching algorithm[J]. Communications of the ACM,1977,10: 762-772.
以前咱們剛開始說壞字符的時候,是否是有可能會出現負值的狀況,即往左移動的狀況,因此咱們爲了解決這個問題,咱們能夠分別計算好後綴和壞字符日後滑動的位數**(好後綴不爲 0 的狀況)**,而後取兩個數中最大的,做爲模式串日後滑動的位數。
這破圖畫起來是真費勁啊。下面咱們來看一下算法代碼,代碼有點長,我都標上了註釋也在網站上 AC 了,若是各位感興趣能夠看一下,不感興趣理解壞字符和好後綴規則便可。能夠直接跳到 KMP 部分
class Solution {
public int strStr(String haystack, String needle) {
char[] hay = haystack.toCharArray();
char[] need = needle.toCharArray();
int haylen = haystack.length();
int needlen = need.length;
return bm(hay,haylen,need,needlen);
}
//用來求壞字符狀況下移動位數
private static void badChar(char[] b, int m, int[] bc) {
//初始化
for (int i = 0; i < 256; ++i) {
bc[i] = -1;
}
//m 表明模式串的長度,若是有兩個 a,則後面那個會覆蓋前面那個
for (int i = 0; i < m; ++i) {
int ascii = (int)b[i];
bc[ascii] = i;//下標
}
}
//用來求好後綴條件下的移動位數
private static void goodSuffix (char[] b, int m, int[] suffix,boolean[] prefix) {
//初始化
for (int i = 0; i < m; ++i) {
suffix[i] = -1;
prefix[i] = false;
}
for (int i = 0; i < m - 1; ++i) {
int j = i;
int k = 0;
while (j >= 0 && b[j] == b[m-1-k]) {
--j;
++k;
suffix[k] = j + 1;
}
if (j == -1) prefix[k] = true;
}
}
public static int bm (char[] a, int n, char[] b, int m) {
int[] bc = new int[256];//建立一個數組用來保存最右邊字符的下標
badChar(b,m,bc);
//用來保存各類長度好後綴的最右位置的數組
int[] suffix_index = new int[m];
//判斷是不是頭部,若是是頭部則true
boolean[] ispre = new boolean[m];
goodSuffix(b,m,suffix_index,ispre);
int i = 0;//第一個匹配字符
//注意結束條件
while (i <= n-m) {
int j;
//從後往前匹配,匹配失敗,找到壞字符
for (j = m - 1; j >= 0; --j) {
if (a[i+j] != b[j]) break;
}
//模式串遍歷完畢,匹配成功
if (j < 0) {
return i;
}
//下面爲匹配失敗時,如何處理
//求出壞字符規則下移動的位數,就是咱們壞字符下標減最右邊的下標
int x = j - bc[(int)a[i+j]];
int y = 0;
//好後綴狀況,求出好後綴狀況下的移動位數,若是不含有好後綴的話,則按照壞字符來
if (y < m-1 && m - 1 - j > 0) {
y = move(j, m, suffix_index,ispre);
}
//移動
i = i + Math.max(x,y);
}
return -1;
}
// j表明壞字符的下標
private static int move (int j, int m, int[] suffix_index, boolean[] ispre) {
//好後綴長度
int k = m - 1 - j;
//若是含有長度爲 k 的好後綴,返回移動位數,
if (suffix_index[k] != -1) return j - suffix_index[k] + 1;
//找頭部爲好後綴子串的最大長度,從長度最大的子串開始
for (int r = j + 2; r <= m-1; ++r) {
//若是是頭部
if (ispre[m-r] == true) {
return r;
}
}
//若是沒有發現好後綴匹配的串,或者頭部爲好後綴子串,則移動到 m 位,也就是匹配串的長度
return m;
}
}
複製代碼
咱們來理解一下咱們代碼中用到的兩個數組,由於兩個規則的移動位數,只與模式串有關,與主串無關,因此咱們能夠提早求出每種狀況的移動狀況,保存到數組中。
咱們剛纔講了 BM 算法,雖然不是特別容易理解,可是若是你用心看的話確定能夠看懂的,咱們再來看一個新的算法,這個算法是考研時必考的算法。實際上 BM 和 KMP 算法的本質是同樣的,你理解了 BM 再來理解 KMP 那就是分分鐘的事啦。
咱們先來看一個實例
視頻
爲了讓讀者更容易理解,咱們將指針移動改爲了模式串移動,二者相對與主串的移動是一致的,從新比較時都是從指針位置繼續比較。
經過上面的實例是否是很快就能理解 KMP 算法的思想了,可是 KMP 的難點不是在這裏,不過多思考,認真看理解起來也是很輕鬆的。
在上面的例子中咱們提到了一個名詞,最長公共先後綴,這個是什麼意思呢?下面咱們經過一個較簡單的例子進行描述。
此時咱們在紅色陰影處匹配失敗,綠色爲匹配成功部分,則咱們觀察匹配成功的部分。
咱們來看一下匹配成功部分的全部前綴
咱們的最長公共先後綴以下圖,則咱們須要這樣移動
好啦,看完上面的圖,KMP的核心原理已經基本搞定了,可是咱們如今的問題是,咱們應該怎麼才能知道他的最長公共先後綴的長度是多少呢?怎麼知道移動多少位呢?
剛纔咱們在 BM 中說到,咱們移動位數跟主串無關,只跟模式串有關,跟咱們的 bc,suffix,prefix 數組的值有關,咱們經過這些數組就能夠知道咱們每次移動多少位啦,其實 KMP 也有一個數組,這個數組叫作 next 數組,那麼這個 next 數組存的是什麼呢?
next 數組存的我們最長公共先後綴中,前綴的結尾字符下標。是否是感受有點彆扭,咱們經過一個例子進行說明。
咱們知道 next 數組以後,咱們的 KMP 算法實現起來就很容易啦,另外咱們看一下 next 數組究竟是幹什麼用的。
剩下的就不用說啦,徹底一致啦,我們將上面這個例子,翻譯成和我們開頭對應的動畫你們看一下。
由於不能夠放置視頻,因此想看視頻的同窗,能夠去看公衆號原文,那裏有視頻
下面咱們看一下代碼,標有詳細註釋,你們認真看呀。
注:不少教科書的 next 數組表示方式不一致,理解便可
class Solution {
public int strStr(String haystack, String needle) {
//兩種特殊狀況
if (needle.length() == 0) {
return 0;
}
if (haystack.length() == 0) {
return -1;
}
// char 數組
char[] hasyarr = haystack.toCharArray();
char[] nearr = needle.toCharArray();
//長度
int halen = hasyarr.length;
int nelen = nearr.length;
//返回下標
return kmp(hasyarr,halen,nearr,nelen);
}
public int kmp (char[] hasyarr, int halen, char[] nearr, int nelen) {
//獲取next 數組
int[] next = next(nearr,nelen);
int j = 0;
for (int i = 0; i < halen; ++i) {
//發現不匹配的字符,而後根據 next 數組移動指針,移動到最大公共先後綴的,
//前綴的後一位,和我們移動模式串的含義相同
while (j > 0 && hasyarr[i] != nearr[j]) {
j = next[j - 1] + 1;
//超出長度時,能夠直接返回不存在
if (nelen - j + i > halen) {
return -1;
}
}
//若是相同就將指針同時後移一下,比較下個字符
if (hasyarr[i] == nearr[j]) {
++j;
}
//遍歷完整個模式串,返回模式串的起點下標
if (j == nelen) {
return i - nelen + 1;
}
}
return -1;
}
//這一塊比較難懂,不想看的同窗能夠忽略,瞭解大體含義便可,或者本身調試一下,看看運行狀況
//我會每一步都寫上註釋
public int[] next (char[] needle,int len) {
//定義 next 數組
int[] next = new int[len];
// 初始化
next[0] = -1;
int k = -1;
for (int i = 1; i < len; ++i) {
//咱們此時知道了 [0,i-1]的最長先後綴,可是k+1的指向的值和i不相同時,咱們則須要回溯
//由於 next[k]就時用來記錄子串的最長公共先後綴的尾座標(即長度)
//就要找 k+1前一個元素在next數組裏的值,即next[k+1]
while (k != -1 && needle[k + 1] != needle[i]) {
k = next[k];
}
// 相同狀況,就是 k的下一位,和 i 相同時,此時咱們已經知道 [0,i-1]的最長先後綴
//而後 k - 1 又和 i 相同,最長先後綴加1,便可
if (needle[k+1] == needle[i]) {
++k;
}
next[i] = k;
}
return next;
}
}
複製代碼
做者:袁廚 原文公衆號:袁廚的算法小屋 我是袁廚,一個酷愛作飯的程序員,一個愛用動圖解算法的年輕人,新人求支持呀。 這篇文章真的寫了好久好久,以爲還不錯的話,就麻煩您點個贊吧,你們也能夠去個人公衆號看個人全部文章,每一個都有動圖解析,公衆號:袁廚的算法小屋