字符串匹配之KMP(KnuthMorrisPratt)算法(圖解)

文章目錄

  • 最長相等前后綴
  • next數組
    • 概念
    • 代碼實現
    • 圖解GetNext中的回溯
    • 改進
    • 代碼實現
  • 代碼
  • 復雜度分析


最長相等前后綴

給出一個字符串 ababa

前綴集合:{a, ab, aba, abab}

后綴集合:{a, ba, aba, baba}

相等前后綴 即上面用同樣顏色標識出來的集合元素,最長相等前后綴 也就是所有 相等前后綴 中最長的那一個,也就是上面的 aba 。用圖片舉例:
在這里插入圖片描述
最長相等前后綴 就是 KMP 算法滑動的依據。我們用 next 數組存儲 最長相等前后綴,以避免每次需要用到 最長相等前后綴 時都需要遍歷尋找的繁瑣。


next數組

概念

next[i]=j 的含義是:下標 i 之前的字符串其 最長相等前后綴 的長度為 jnext[0]= -1 (前面沒有字符串單獨處理)。

ababacd
next[0] = -1next[1] = 0next[2] = 0next[3] = 1next[4] = 2next[5] = 3next[6] = 0

在這里插入圖片描述
s1[5] != s2[5] 時,移動 s2,讓 s2 的前綴(ababa)匹配 s1 的后綴(ababa),即比較 s1[5]s2[next[5]] 。移動的距離是 不匹配位置下標相等前綴 之間的字符數量,即 5-3=2
在這里插入圖片描述

從上面的例子中可以看出,next 的作用有兩個:

  1. 表示該處字符不匹配時應該回溯到的字符的下標。
  2. 上文提到的:下標 i 之前的字符串其 最長相等前后綴 的長度。

代碼實現

class Solution {public:void GetNext(const string& s, vector<int>& next) {int i = 0, j = -1;next[0] = -1; // 下標為0的字符前沒有字符串while (i < next.size() - 1) { // 因為函數體中每次先對i++,再對next[i]進行賦值// 因此i需要小于next.size() - 1,以保證自增時不越界if (j == -1 || s[i] == s[j]) {i++;j++; /* 關于 j *//*s[i] == s[j]成立時,next[i] 在 next[i - 1] 的值(j)的基礎上 + 1換言之,也就意味著相等前后綴的長度+1,新后綴結尾 i+1 對應的前綴結尾為 j+1*//* j == -1成立時,說明不存在相等前后綴,因此 i 之前的字符串的相等前后綴長度為 next[i] = (-1)++ = 0 */ next[i] = j;}else {j = next[j];// next[j] 是回溯的位置,是 j 指向的字符 之前的字符串的最長相等前后綴的長度// 該操作為了將前綴移動到后綴的位置上,假設 相等長度為 m// 相當于將 (0, j-m)、(1, j-m+1)...(m-1, j-1)匹配上// 舉個例子:// 字符串:a  b a b a c d// next:  -1 0 0 1 2 3//                j   i// 由于 j 指向的 字符b 其之前的 字符串 aba 最長相等前后綴的長度為 1,// 下標1 作為 新j 就將(0, j-1)匹配上了// 換言之,只需要將 下標1 作為 新j 即可將求 ababac 最長相等前后綴問題轉換為// 求 abac 最長相等前后綴的問題。}}}void getNext(const string& pattern, vector<int>& next){ // 另一種寫法int i, j = 0;next[0] = -1;   //第一個位置不存在數據,為-1for (int i = 1; i < next.size(); i++){//如果當前位置沒有匹配前綴,則回溯到求當前后綴的最長可匹配前綴while (j != 0 && pattern[j] != pattern[i]){j = next[j];}//如果該位置匹配,則在next數組在上一個位置的基礎上加一if (pattern[j] == pattern[i]){j++;}next[i] = j;}}
};

關于提到的另一種寫法,這里不多做分析,可以閱讀凌桓大佬的博客。


圖解GetNext中的回溯

舉個直觀的例子:

  • 紅色部分分別為:最長相等前、后綴。

  • 藍色部分為雙指針指向的,待匹配的元素。

  • 黑色部分為未開始匹配的部分。

  • 綠色部分為 next 數組。
    在這里插入圖片描述

  • 如果 s[i] == s[j] ,雙指針同時后移,紅色區域變大。

  • 如果不匹配,必須在 紅色部分 重新尋找 相等前后綴,新的相等前后綴長度必然縮短。

紫色部分是紅色部分的最長相等前后綴,可以看到,四個紫色部分都是完全相等的。同時,改變 j 的指向,回溯后 j = next[j]
在這里插入圖片描述

  1. 此時,若 s[i] == s[j],又因 j 前面的紫色部分和 i 前面的紫色部分完全相等。則最長相等前后綴長度+1。
  2. 不等則進行下一次回溯。圖中下一次回溯時不再有相等前后綴,因此不再有紫色部分,不斷地回溯,直到 j 指向 -1,此時觸發判定條件,執行 j++; i++; next[i]=j;

改進

舉個例子:

  • 主串 s = “aaaabaaaaac”
  • 子串 t = “aaaac”

bc 不匹配時應該 b 與下標為 next[c] = 3 的元素 a 比,這顯然是不匹配的,繼續回溯,next[next[c]] 回溯后的字符依然是 a 。此時已經 沒有必要再將進行回溯了。

節省效率的做法應該是當 bnext[3] 不匹配時,就直接回溯到首個 anext 指向(即下標 -1)。即:

下標01234
元素aaaac
next-10123
fail-1-1-1-13

規則:

  • 如果 i 位字符與它 next 值指向的 j 位字符相等,則該 i 位的 fail 就指向 j 位的 fail 值;
  • 如果不等,則該 i 位的 fail 值就是它自己的 next 值。

舉個其他例子 ababaaab,進一步體會:

下標01234567
元素ababaaab
next-10012311
fail-10-10-1310

代碼實現

這里用 next 表示上面提到的 fail 數組。

void getNext(const string& s, vector<int>& fail) {int i = 1, j = 0;fail[0] = -1; // 下標為0的字符前沒有字符串while (i < fail.size()) {if (s[i] != s[j]) { // 字符不匹配fail[i] = j; // 不等時,fail[i] = next[i] = jj = fail[j]; // 回溯}else { // 字符匹配fail[i] = fail[j]; // i 指向的字符與 j 指向字符相等}j++;i++;}
}

輸出結果:
在這里插入圖片描述

在這里插入圖片描述


代碼

class Solution {
public:void GetNext(const string& s, vector<int>& next) {int i = 0, j = -1;next[0] = -1; // 下標為0的字符前沒有字符串while (i < next.size()-1) { if (j == -1 || s[i] == s[j]) {i++;j++;next[i] = j;}else {// 如果當前位置沒有匹配前綴,則回溯到求當前后綴的最長可匹配前綴j = next[j];}}}void getNext(const string& s, vector<int>& fail) { // 改進的next數組int i = 1, j = 0;fail[0] = -1; // 下標為0的字符前沒有字符串while (i < fail.size()) {if (s[i] != s[j]) { // 字符不匹配fail[i] = j; // 不等時,fail[i] = next[i] = jj = fail[j]; // 回溯}else { // 字符匹配fail[i] = fail[j]; // i 指向的字符與 j 指向字符相等}j++;i++;}}int knuthMorrisPratt(const string& query, const string& pattern) {//不滿足條件則直接返回falseif (query.empty() || pattern.empty() || query.size() < pattern.size()){return -1;}int i = 0, j = 0;int len1 = query.size(), len2 = pattern.size();vector<int> next(pattern.size(), -1); // next數組GetNext(pattern, next);while (i < len1 && j < len2){if (j == -1 || query[i] == pattern[j]){i++;j++; // i、j各增1}else j = next[j]; // i不變,j回溯}if (j == len2)return(i - len2); // 返回匹配模式串的首字符下標elsereturn -1; // 返回不匹配標志}
};

復雜度分析

  • 空間復雜度O(M): 需要借助到一個 next 數組,數組長度為 MMMMMM 為模式串長度。
  • 時間復雜度O(N + M): 時間復雜度主要包含兩個部分,next 數組的構建以及對主串的遍歷:next 數組構建的時間復雜度為 O(M);后面匹配中主串不回溯,循環時間復雜度為 O(N),所以 KMP 算法的時間復雜度為 O(N + M)。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/443718.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/443718.shtml
英文地址,請注明出處:http://en.pswp.cn/news/443718.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

linux下tomcat6.0與jdk安裝詳細步驟

安裝Tomcat6.0和JDK1.6 在linux系統上安裝tomcat和jdk應該說是我學習linux知識的第一課了&#xff0c;之前只 是聽說過&#xff0c;從沒接觸過&#xff0c;但我們公司項目都是部署在linux系統上的&#xff0c;那天上司突 然給我發了幾個文檔&#xff0c;讓我看一下&#xff…

Android入門(一) | Android Studio的配置與使用

文章目錄安裝配置Android Studio使用Android Studio模擬器更改Android SDK的路徑Hello World&#xff01;安裝配置Android Studio 從這一步開始&#xff1a; 一直點 next 即可&#xff0c;直到存儲路徑的選擇上&#xff0c;可以放到非 C 盤&#xff0c;這里我放到 D 盤了&am…

Android 入門(四) | Intent 實現 Activity 切換

文章目錄Intent顯式 Intent定義兩個 xml 文件android:orientationmatch_parent 和 wrap_contentIntent函數定義兩個 Activity隱式 Intent更多隱式 Intent 的用法用隱式 Intent 打開系統瀏覽器自建 Activity 以響應打開網頁的 Intent向下一個活動傳遞數據返回數據給上一個活動In…

Android入門(二) | 項目目錄及主要文件作用分析

文章目錄項目目錄分析app目錄分析AndroidManifest.xml 分析MainActivity.kt 分析build.gradle 分析最外層目錄下的 build.gradleapp 目錄下的 build.gradle項目目錄分析 我們來看一下 src/main/res 下的一些文件&#xff1a; .gradle 和 .idea &#xff1a;這兩個目錄下放置…

Android入門(三) | Android 的日志工具 Logcat

文章目錄日志工具類 android.util.LogLogcat 中的過濾器日志工具類 android.util.Log Log 從屬日志工具類 android.util.Log &#xff0c;該類提供了五個方法供我們打印日志&#xff1a; Log.v() &#xff1a;用于打印那些最為瑣碎的、意義最小的日志信息。對應級別 verbose&…

Android 客戶端與服務器交互方式

突然想到一個問題就是Android客戶端與服務器交互有幾種方式&#xff0c;因為在腦袋里想當然的就是webservices和json。要在Android手機客戶端與pc服務器交互&#xff0c;需要滿足下面幾種條件&#xff1a;跨平臺、傳輸數據格式標準、交互方便...。 為了與服務器通訊其實無非就…

Android入門(五) | Activity 的生命周期

文章目錄Activity 的狀態及生命周期實現管理生命周期FirstActivitySecondActivityDialogActivity運行結果舊活動被回收了還能返回嗎&#xff1f;Activity 的狀態及生命周期 Android 的應用程序運用 棧&#xff08;Back Stack&#xff09; 的思想來管理 Activity&#xff1a; …

Android入門(六) | Activity 的啟動模式 及 生產環境中關于 Activity 的小技巧

文章目錄Activity 的啟動模式standardsingleTopsingleTasksingleInstance技巧了解當前界面是哪個 Activity隨時隨地退出程序啟動活動的最佳寫法Activity 的啟動模式 standard&#xff1a;默認的啟動方式&#xff0c;每次啟動一個活動都會重新創建singleTop&#xff1a;如果該活…

Android入門(七) | 常用控件

文章目錄TextView 控件&#xff1a;文本信息Button 控件&#xff1a;按鈕EditText 控件&#xff1a;輸入框ImageView 控件&#xff1a;圖片ProgressBar 控件&#xff1a;進度條AlertDialog 控件&#xff1a;提示框ProgressDialog 控件&#xff1a;帶有進度條的提示框TextView 控…

Android入門(八) | 常用的界面布局 及 自定義控件

文章目錄LinearLayout &#xff1a;線性布局android:layout_gravity &#xff1a;控件的對齊方式android:layout_weight&#xff1a;權重RelativeLayout &#xff1a;相對布局相對于父布局進行定位相對于控件進行定位邊緣對齊FrameLayout &#xff1a;幀布局Percent &#xff1…

Android入門(九)| 滾動控件 ListView 與 RecyclerView

文章目錄ListView內置類型的簡單運用定制數據類型提升效率點擊事件RecyclerView布局管理器點擊事件ListView 內置類型的簡單運用 由于手機屏幕空間有限&#xff0c;能夠一次性在屏幕上顯示的內容不多&#xff0c;當我們的程序有大量數據需要顯示的時候就可以借助 ListView 來…

關于“三門問題”的一些想法

三門問題&#xff08;Monty Hall problem&#xff09;亦稱為蒙提霍爾問題、蒙特霍問題或蒙提霍爾悖論&#xff0c;大致出自美國的電視游戲節目Let’s Make a Deal。問題名字來自該節目的主持人蒙提霍爾&#xff08;Monty Hall&#xff09;。參賽者會看見三扇關閉了的門&#xf…

Android入門(10)| Fragment碎片詳解

文章目錄為什么要使用碎片&#xff08;Fragment&#xff09;實例布局文件FragmentActivity動態添加碎片布局文件FragmentActivity碎片通信Fragment布局文件Activity生命周期為什么要使用碎片&#xff08;Fragment&#xff09; 我們在手機上看新聞可能是這樣的&#xff1a; Re…

Android開發(1) | Fragment 的應用——新聞應用

文章目錄Item&#xff1a;標題子項布局文件Java代碼標題碎片布局文件Java代碼新聞內容碎片布局文件Java代碼新聞內容活動布局文件Java代碼首界面布局文件Java代碼Item&#xff1a;標題子項 布局文件 news_item.xml&#xff1a; <TextViewxmlns:android"http://schema…

Java Web整體異常處理

在實際的J2EE項目中&#xff0c;系統內部難免會出現一些異常&#xff0c;就如StrutsSpringHibernate項目&#xff1a;通常一個頁面請求到達后臺以后&#xff0c;首先是到action&#xff08;就是MVC中的controller&#xff09;&#xff0c;在action層會調用業務邏輯層service&am…

Android入門(11)| 全局廣播與本地廣播

文章目錄廣播概念接收廣播動態注冊實例靜態注冊實例發送廣播發送標準廣播廣播的跨進程特性發送有序廣播本地廣播廣播概念 Android 中的每個應用程序都可以對自己感興趣的廣播進行注冊&#xff0c;這樣該程序就只會接收到自己所關心的廣播內容&#xff0c;這些廣播可能是來自系…

Android開發(2) | 廣播 Broadcast 的應用——強制下線功能

文章目錄功能簡介關閉所有活動登陸界面發送強制下線的廣播廣播接收器AndroidManifest.xml運行結果功能簡介 強制下線功能只需要彈出一個對話框&#xff0c;讓用戶只能點擊確定按鈕&#xff0c;回到登錄界面。 如果在每一個活動中添加一個對話框的話太過繁瑣&#xff0c;用廣播…

Android入門(12)| 數據持久化

文章目錄數據持久化文件存儲將數據存儲進文件實例從文件中讀取數據實例SharedPreferences存儲將數據存儲進文件實例從文件中讀取數據實例實現記住密碼的功能SQLite數據庫存儲創建自己的幫助類調用自己的幫助類補全 onUpgrade() 方法增刪查改增&#xff1a;SQLiteDatabase.inser…

Android入門(13)| Android權限 與 內容提供器

文章目錄普通權限與危險權限運行時申請權限內容提供器運用安卓封裝好的內容提供器自實現的內容提供器概念實現普通權限與危險權限 主要用于不同應用程序之間在保證被訪數據的安全性的基礎上&#xff0c;實現數據共享的功能。 在 Android 6.0 開始引入了運行時權限的功能&…

Java實現身份證號碼的驗證,JAVA后臺驗證身份證號碼

代碼如下&#xff1a; package cn.gov.csrc.util;/*** 18 位身份證驗證器* * author admin* */ public class IDCard {final int[] wi { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2, 1 };final int[] vi { 1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2 };private int[] ai n…