算法---KMP算法

字符串

  • 1 KMP算法
    • 狀態機概述
    • 構建狀態轉移

1 KMP算法

原文鏈接:https://zhuanlan.zhihu.com/p/83334559

先約定,本文用pat表示模式串,長度為M,txt表示文本串,長度為N,KMP算法是在txt中查找子串pat,如果找到,返回這個子串的起始索引,否則返回-1.

用一張圖演示一下KMP算法:
請添加圖片描述
KMP算法永不回退txt中的索引i,不走回頭路。而是借助一個數組dp,dp數組中存儲的信息把pat移到正確的位置繼續匹配。

KMP的難點在于計算dp數組,計算這個dp數組,之和pat有關。意思是說,只要給我個pat,我就能通過這個模式串計算出dp數組,與txt無關系。

dp 數組只和 pat 有關,那么我們這樣設計 KMP 算法就會比較漂亮:

int KMP(char *pat,char *txt)
{/*第一步獲取dp數組*/dp = getDP(char *pat);/* 第二步搜索*/return_value = search(char *txt,char *pat);/* 第三步 */return return_value;
}

狀態機概述

為什么說KMP算法與狀態機有關呢?是這樣的,我們可以認為pat的匹配就是狀態的轉移,比如pat=“ABABC”:
請添加圖片描述
如上圖,圓圈內的數字就是狀態,狀態0是起始狀態,狀態5是終止狀態。開始匹配時pat處于起始狀態,一旦轉移到終止狀態,就說明在txt中找到了pat。比如說當前處于狀態2,說明字符AB被匹配。請添加圖片描述
另外,當處于一個狀態時,接下來匹配的字符不同,pat 狀態轉移的行為也不同。比如說假設現在匹配到了狀態 4,如果遇到字符 A 就應該轉移到狀態 3,遇到字符 C 就應該轉移到狀態 5,如果遇到字符 B 就應該轉移到狀態 0:
請添加圖片描述

KMP算法最關鍵的步驟就是構造這個狀態轉移,要確定狀態轉移的行為,得確定兩個變量,一個是當前的匹配狀態,另一個是遇到的字符。確定了這兩個變量后,就可以知道這個情況下應該轉移到那個狀態。

為了描述狀態轉移圖,我們定義一個二維 dp 數組,它的含義如下:

dp[j][c] = next
0 <= j < M,代表當前的狀態
0 <= c < 256,代表遇到的字符(ASCII 碼)
0 <= next <= M,代表下一個狀態dp[4]['A'] = 3 表示:
當前是狀態 4,如果遇到字符 A,
pat 應該轉移到狀態 3
因為狀態3之前的字符和狀態4之前的字符相同dp[1]['B'] = 2 表示:
當前是狀態 1,如果遇到字符 B,
pat 應該轉移到狀態 2

根據我們這個dp數組和剛才狀態轉移的過程,我們可以先寫出KMP算法的search函數:

int search(char *txt,char *pat)
{int M = strlen(pat);int N = strlen(txt);//pat的初始態為0int  j = 0;for(int i=0;i<N;i++){/* 當前狀態是j,遇到字符txt[i],pat應該轉移到那個狀態? */j = dp[j][txt[i]];/* 如果到達終止態,返回匹配開頭的索引 */if(j==M) return i-M+1;}/* 沒有到達終止態,返回-1表示匹配失敗 */return -1;
}

構建狀態轉移

回想剛才說的:要確定狀態轉移的行為,必須明確兩個變量,一個是當前的匹配狀態,另一個是遇到的字符,而且我們已經根據這個邏輯確定了 dp 數組的含義,那么構造 dp 數組的框架就是這樣:

for 0 <= j < M: # 狀態for 0 <= c < 256: # 字符dp[j][c] = next

這個next狀態應該怎么求呢?顯然,如果遇到的字符c和pat[j]匹配,狀態就應該向前推進一個,也就是說next=j+1,我們不妨稱這種情況為狀態推進:

請添加圖片描述
如果字符c和pat[j]不匹配,狀態就要回退(或者原地不動),我們不妨稱這種情況為狀態重啟:
請添加圖片描述
那么,如何得知在哪個狀態重啟呢?解答這個問題之前,我們再定義一個名字:影子狀態(我編的名字),用變量 X 表示。所謂影子狀態,就是和當前狀態具有相同的前綴。比如下面這種情況:
請添加圖片描述
當前狀態j=4,其影子狀態為X=2,它們都有相同的前綴AB,因為狀態X和狀態j存在相同的前綴,所以當狀態j準備進行狀態重啟(遇到的字符c和pat[j]不匹配)的時候,可以通過X的狀態轉移來獲得最近的重啟位置。
比如說剛才的情況,如果狀態 j 遇到一個字符 “A”,應該轉移到哪里呢?首先只有遇到 “C” 才能推進狀態,遇到 “A” 顯然只能進行狀態重啟。狀態 j 會把這個字符委托給狀態 X 處理,也就是 dp[j]['A'] = dp[X]['A']
請添加圖片描述

為什么這樣可以呢?因為:既然 j 這邊已經確定字符 “A” 無法推進狀態,只能回退,而且 KMP 就是要盡可能少的回退,以免多余的計算。那么 j 就可以去問問和自己具有相同前綴的 X,如果 X 遇見 “A” 可以進行「狀態推進」,那就轉移過去,因為這樣回退最少。

所以計算bp數組的偽代碼就有了:

int X # 影子狀態
for 0 <= j < M:for 0 <= c < 256:if c == pat[j]:# 狀態推進dp[j][c] = j + 1else: # 狀態重啟# 委托 X 計算重啟位置dp[j][c] = dp[X][c] # 更新 XX = dp[X][c]

為什么更新X使用的是: X = dp[X][c],我們首先要理解dp的含義。看下圖:

請添加圖片描述

dp[4]['A'] = 3 表示:
當前是狀態 4,如果遇到字符 A,
pat 應該轉移到狀態 3
因為狀態3之前的字符和狀態4之前的字符相同

狀態3前面的字符是ABA,狀態四前面有ABA(最后一個A是要匹配的),所以dp[4]['A'] = 3dp[狀態n][匹配的字符y]的值實際代表的含義是在轉態n下pat字符串前面和后面能匹配的字符數(也就是前面說的影子)。

X = dp[X][c]表示在原來狀態下匹配字符c,如果相同的話,影子狀態就是增加1,如上圖,假設現在在狀態3匹配,則X=1,接下來就會計算dp[4]狀態下的轉移,在計算狀態4轉移前,首先要計算出狀態4對應的影子狀態,dp[X][c]就是比較X=1對應的字符B和狀態3對應的字符B是否相同,如果相同就是要增加影子長度,不相同就要轉移,計算對應的影子。

所以,getDP函數實現如下:

void getDP(char *pat,int M)
{/* dp[狀態][字符] = 下個狀態 */dp[0][pat[0]]= 1;int X=0;for(int j=1;j<M;j++){for(int c=0;c<256;c++){/* 和字符c匹配了 */if(pat[j]==c)dp[j][c]=j+1;else   /* 和字符c不匹配 */dp[j][c]=dp[X][c];}/* 更新 X*/X = dp[X][pat[j]];}
}

我們整合并化簡兩個程序,形成一個KMP算法。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>int dp[256][256];void getDP(char *pat,int M)
{/* dp[狀態][字符] = 下個狀態 */dp[0][pat[0]]= 1;int X=0;for(int j=1;j<M;j++){for(int c=0;c<256;c++){/* 和字符c匹配了 */if(pat[j]==c)dp[j][c]=j+1;else   /* 和字符c不匹配 */dp[j][c]=dp[X][c];}/* 更新 X*/X = dp[X][pat[j]];}
}int search(char *txt,char *pat)
{int M = strlen(pat);int N = strlen(txt);//pat的初始態為0int  j = 0;for(int i=0;i<N;i++){/* 當前狀態是j,遇到字符txt[i],pat應該轉移到那個狀態? */j = dp[j][txt[i]];/* 如果到達終止態,返回匹配開頭的索引 */if(j==M) return i-M+1;}/* 沒有到達終止態,返回-1表示匹配失敗 */return -1;
}int main(void)
{char *txt = "BCDABABC";char *pat = "ABABC";getDP(pat,strlen(pat));int count = search(txt,pat);printf("count = %d\n",count);return 0;
}

在這里插入圖片描述

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

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

相關文章

cache初接觸,并利用了DataView

我們在寫代碼的時候,如果數據控件要獲得數據,一般方法,Conn.Open();OleDbCommand cmd;cmd new OleDbCommand(sql, Conn);GridView1.DataSource dbcenter.accessGetDataSet(sql);GridView1.DataBind();Conn.close();但如果多個數據控件要綁定數據,則比較頻繁打開數據庫,效率一…

Java ByteArrayInputStream reset()方法及示例

ByteArrayInputStream類reset()方法 (ByteArrayInputStream Class reset() method) reset() method is available in java.util package. reset()方法在java.util包中可用。 reset() method is used to reset this ByteArrayInputStream to the last time marked position and …

回文數猜想

問題描述&#xff1a; 一個正整數&#xff0c;如果從左向右讀&#xff08;稱之為正序數&#xff09;和從右向左讀&#xff08;稱之為倒序數&#xff09;是一樣的&#xff0c;這樣的數就叫回文數。任取一個正整數&#xff0c;如果不是回文數&#xff0c;將該數與他的倒序數相加…

文件上傳 帶進度條(多種風格)

文件上傳 帶進度條 多種風格 非常漂亮&#xff01; 友好的提示 以及上傳驗證&#xff01; 部分代碼&#xff1a; <form id"form1" runat"server"><asp:ScriptManager ID"scriptManager" runat"server" EnablePageMethods&quo…

同步---自旋鎖

1 自旋鎖的基本概念 自旋鎖最多只能被一個可執行線程持有&#xff0c;如果一個執行線程試圖獲得一個已經被使用的自旋鎖&#xff0c;那么該線程就會一直進行自旋&#xff0c;等待鎖重新可用。在任何時刻&#xff0c;自旋鎖都可以防止多余一個的執行線程同時進入臨界區。 Linu…

實習日志----4.播放時段參數設置

由于客戶在下發廣告時&#xff0c;一則廣告可在多個時段播放&#xff0c;這就需要設置多個播放時段的參數。 但在這種情況下&#xff0c;我并不知道用戶每次需要下發幾個時段&#xff0c;所以前臺不能設定死。 因此我要實現這么一個功能&#xff0c;讓用戶根據自己的需要來動態…

線性插值算法實現圖像_C程序實現插值搜索算法

線性插值算法實現圖像Problem: 問題&#xff1a; We are given an array arr[] with n elements and an element x to be searched amongst the elements of the array. 給定一個數組arr []&#xff0c;其中包含n個元素和一個要在該數組的元素中搜索的元素x 。 Solution: 解&…

hdu 1197

地址&#xff1a;http://acm.hdu.edu.cn/showproblem.php?pid1197 題意&#xff1a;求一個數轉換成10&#xff0c;12&#xff0c;16進制后各個位上的數的和是否相等。 mark&#xff1a;模擬進制轉換。 代碼&#xff1a; #include <stdio.h>int zh(int a, int n) {int su…

linux系統編程---線程總結

線程總結1 線程的實現線程創建線程退出線程等待線程清理2 線程的屬性線程的分離線程的棧地址線程棧大小線程的調度策略線程優先級3 線程的同步互斥鎖讀寫鎖條件變量信號量線程是系統獨立調度和分配的基本單位。同一進程中的多個線程將共享該進程中的全部系統資源&#xff0c;例…

博客上一些項目相關源碼鏈接

GitHub&#xff1a;https://github.com/beyondyanyu/Sayingyy

重新開啟Ctrl+Alt+Backspace快捷鍵

UBUNTU老用戶知道CtrlAltBackspace這個快捷鍵是用來快速重啟X的在9.04中被默認關閉了&#xff0c;那如何來打開它呢&#xff1f;在終端中輸入&#xff1a;sudo gedit /etc/X11/xorg.conf在其中加入&#xff1a;Section “ServerFlags”Option “DontZap” “false”EndSection退…

Java LocalDate類| 帶示例的getDayOfYear()方法

LocalDate類的getDayOfYear()方法 (LocalDate Class getDayOfYear() method) getDayOfYear() method is available in java.time package. getDayOfYear()方法在java.time包中可用。 getDayOfYear() method is used to get the day-of-year field value of this LocalDate obje…

火腿三明治定理

定理&#xff1a;任意給定一個火腿三明治&#xff0c;總有一刀能把它切開&#xff0c;使得火腿、奶酪和面包片恰好都被分成兩等份。 而且更有趣的是&#xff0c;這個定理的名字真的就叫做“火腿三明治定理”&#xff08;ham sandwich theorem&#xff09;。它是由數學家亞瑟?斯…

如何給Linux操作系統(CentOS 7為例)云服務器配置環境等一系列東西

1.首先&#xff0c;你得去購買一個云服務器&#xff08;這里以阿里云學生服務器為例&#xff0c;學生必須實名認證&#xff09; 打開阿里云&#xff0c;搜索學生服務器點擊進入即可 公網ip為連接云服務器的主機 自定義密碼為連接云服務器是需要輸入的密碼 購買即可 點擊云服…

Linux系統編程---I/O多路復用

文章目錄1 什么是IO多路復用2 解決什么問題說在前面I/O模型阻塞I/O非阻塞I/OIO多路復用信號驅動IO異步IO3 目前有哪些IO多路復用的方案解決方案總覽常見軟件的IO多路復用方案4 具體怎么用selectpollepolllevel-triggered and edge-triggered狀態變化通知(edge-triggered)模式下…

[轉帖]純屬娛樂——變形金剛vs天網

[轉帖]變形金剛2的影評-《變形金剛3 天網反擊戰》有一個問題困擾了我足足二十年&#xff1a;為什么汽車人要幫地球人&#xff1f;光用“所有有感知的生物都應享有自由”這個法則是根本說不過去的&#xff0c;因為豬也有感知&#xff0c;但人類就把豬圈養起來&#xff0c;隨意殺…

c#中textbox屬性_C#.Net中的TextBox.MaxLength屬性與示例

c#中textbox屬性Here we are demonstrating use of MaxLength property of TextBox. 在這里&#xff0c;我們演示了TextBox的MaxLength屬性的使用。 MaxLength property of TextBox is used to set maximum number of character that we can input into a TextBox. Limit of M…

IIS7 MVC網站生成、發布

(1)生成。 確保System.Web.Mvc.dll在bin目錄下 (2)發布網站到文件系統 (3)在IIS中為網站添加應用程序池&#xff08;一個虛擬目錄&#xff0c;一個應用程序池&#xff09; (4)添加在默認網站下添加虛擬目錄 &#xff08;5&#xff09;轉換為應用程序 至此&#xff0c;部署完畢 …

標題:明碼

轉載&#xff1a;https://blog.csdn.net/u011747888/article/details/79781040 標題&#xff1a;明碼 漢字的字形存在于字庫中&#xff0c;即便在今天&#xff0c;16點陣的字庫也仍然使用廣泛。 16點陣的字庫把每個漢字看成是16x16個像素信息。并把這些信息記錄在字節中。 一…

C語言多維數組

文章目錄多維數組數組名下標指向數組的指針作為函數參數的多維數組指針數組小結多維數組 如果某個數組的維數超過1&#xff0c;它就被稱為多維數組&#xff0c;例如&#xff0c;下面這個聲明&#xff1a; int matrix[6][10]創建了一個包含60個元素的矩陣。但是&#xff0c;它…