[轉]數據結構KMP算法配圖詳解(超詳細)

KMP算法配圖詳解

前言

KMP算法是我們數據結構串中最難也是最重要的算法。難是因為KMP算法的代碼很優美簡潔干練,但里面包含著非常深的思維。真正理解代碼的人可以說對KMP算法的了解已經相當深入了。而且這個算法的不少東西的確不容易講懂,很多正規的書本把概念一擺出直接勸退無數人。這篇文章將盡量以最詳細的方式配圖介紹KMP算法及其改進。文章的開始我先對KMP算法的三位創始人Knuth,Morris,Pratt致敬,懂得這個算法的流程后你真的不得不佩服他們的聰明才智。

KMP解決的問題類型

KMP算法的作用是在一個已知字符串中查找子串的位置,也叫做串的模式匹配。比如主串s=“goodgoogle”,子串t=“google”。現在我們要找到子串t 在主串s 中的位置。大家肯定覺得這還不簡單,不就在第五個嘛,一眼就看出來了。
當然,在字符串非常少時,“肉眼觀察法”不失為一個好方法。但如果要你在一千行文本里找一個單詞,我想一般人都會數得崩潰吧。這就讓我想起來考試的時候,如果一兩道選擇題不會。這時候,“肉眼觀察法”可能效果不錯,但是如果好幾道大題不會呢?“肉眼觀察法”就絲毫不起效了。所以打鐵還需自身硬,我們把這種枯燥的事以一定的算法交給計算機處理。
第一種我們容易想到的就是暴力求解法。
這種方法也叫樸素的模式匹配

簡單來說就是:從主串s 和子串t 的第一個字符開始,將兩字符串的字符一一比對,如果出現某個字符不匹配,主串回溯到第二個字符,子串回溯到第一個字符再進行一一比對。如果出現某個字符不匹配,主串回溯到第三個字符,子串回溯到第一個字符再進行一一比對…一直到子串字符全部匹配成功。

下面我們通過圖片展示這個過程:
豎直線表示相等,閃電線表示不等
第一個過程:子串“goo”部分與主串相等,'g’不等,結束比對,進行回溯。
在這里插入圖片描述
第二個過程:開始時就不匹配,直接回溯
在這里插入圖片描述
第三個過程:開始時即不匹配,直接回溯
在這里插入圖片描述
第四個過程:開始時即不匹配,直接回溯
在這里插入圖片描述
第五個過程:匹配成功
在這里插入圖片描述
大家可能會想:這個方法也太慢了吧!求一個子串位置需要太多的步驟。而且很多步驟根本不必要進行。
這個想法非常好!!很多偉大的思想都是在一步步完善更正已有方法中誕生的。這種算法在最好情況下時間復雜度為O(n)。即子串的n個字符正好等于主串的前n個字符,而最壞的情況下時間復雜度為O(m*n)。相比而言這種算法空間復雜度為O(1),即不消耗空間而消耗時間。

下面就開始進入我們的正題:KMP算法是怎樣優化這些步驟的。其實KMP的主要思想是:“空間換時間”。
大家打起精神,認真看下面的內容
首先,為什么樸素的模式匹配這么慢呢?
你再回頭看一遍就會發現,哦,原來是回溯的步驟太多了。所以我們應該盡量減少回溯的次數。
怎樣做呢?比如上面第一個圖:當字符’d’與’g’不匹配,我們保持主串的指向不變,
主串依然指向’d’,而把子串進行回溯,讓’d’與子串中’g’之前的字符再進行比對。
如果字符匹配,則主串和子串字符同時右移。
至于子串回溯到哪個字符,這個問題我們先放一放。

我先提出一個概念:一個字符串最長相等前綴和后綴。
教科書常用的手段是:在此處擺出一堆數學公式讓大家自行理解。
在這里插入圖片描述
這也是為什么看計算機學科的書沒有較好的數學基礎會很痛苦。(當初我為什么不好好學數學T_T)
大家先不要強行理解數學公式,且聽我慢慢道來:
我給大家個例子。
字符串 abcdab
前綴的集合:{a,ab,abc,abcd,abcda}
后綴的集合:{b,ab,dab,cdab,bcdab}
那么最長相等前后綴不就是ab嘛.

做個小練習吧:
字符串:abcabfabcab中最長相等前后綴是什么呢:
對就是abcab
好了我們現在會求一個字符串的前綴,后綴以及最長相等前后綴了。
這個概念很重要。到這里如果都看懂了,可以鼓勵一下自己,然后回想一遍,再往下看。
之前留了一個問題,子串回溯到哪個字符,現在可以著手解決了。

圖解KMP

現在我們先看一個圖:第一個長條代表主串,第二個長條代表子串。紅色部分代表兩串中已匹配的部分,
綠色和藍色部分分別代表主串和子串中不匹配的字符。
再具體一些:這個圖代表主串"abcabeabcabcmn"和子串"abcabcmn"。
在這里插入圖片描述
在這里插入圖片描述
現在發現了不匹配的地方,根據KMP的思想我們要將子串向后移動,現在解決要移動多少的問題。
之前提到的最長相等前后綴的概念有用處了。因為紅色部分也會有最長相等前后綴。如下圖:
在這里插入圖片描述
在這里插入圖片描述
灰色部分就是紅色部分字符串的最長相等前后綴,我們子串移動的結果就是讓子串的紅色部分最長相等前綴和主串紅色部分最長相等后綴對齊。
在這里插入圖片描述
在這里插入圖片描述
這一步弄懂了,KMP算法的精髓就差不多掌握了。接下來的流程就是一個循環過程了。事實上,每一個字符前的字符串都有最長相等前后綴,而且最長相等前后綴的長度是我們移位的關鍵,所以我們單獨用一個next數組存儲子串的最長相等前后綴的長度。而且next數組的數值只與子串本身有關。
所以next[i]=j,含義是:下標為i 的字符前的字符串最長相等前后綴的長度為j。
我們可以算出,子串t= "abcabcmn"的next數組為next[0]=-1(前面沒有字符串單獨處理)
next[1]=0;next[2]=0;next[3]=0;next[4]=1;next[5]=2;next[6]=3;next[7]=0;

abcabcmn
next[0]next[1]next[2]next[3]next[4]next[5]next[6]next[7]
-10001230

本例中的藍色c處出現了不匹配(是s[5]!=t[5]),
在這里插入圖片描述
我們把子串移動,也就是讓s[5]與t[5]前面字符串的最長相等前綴后一個字符再比較,而該字符的位置就是t[?],很明顯這里的?是2,就是不匹配的字符前的字符串 最長相等前后綴的長度。
在這里插入圖片描述
也是不匹配的字符處的next數組next[5]應該保存的值,也是子串回溯后應該對應的字符的下標。 所以?=next[5]=2。接下來就是比對是s[5]和t[next[5]]的字符。這里也是最奇妙的地方,也是為什么KMP算法的代碼可以那么簡潔優雅的關鍵。
我們可以總結一下,next數組作用有兩個:
一是之前提到的,

next[i]的值表示下標為i的字符前的字符串最長相等前后綴的長度。

二是:

表示該處字符不匹配時應該回溯到的字符的下標

next有這兩個作用的源頭是:之前提到的字符串的最長相等前后綴
想一想是不是覺得這個算法好厲害,從而不得不由衷佩服KMP算法的創始人們。

KMP算法的時間復雜度

現在我們分析一下KMP算法的時間復雜度:
KMP算法中多了一個求數組的過程,多消耗了一點點空間。我們設主串s長度為n,子串t的長度為m。求next數組時時間復雜度為O(m),因后面匹配中主串不回溯,比較次數可記為n,所以KMP算法的總時間復雜度為O(m+n),空間復雜度記為O(m)。相比于樸素的模式匹配時間復雜度O(m*n),KMP算法提速是非常大的,這一點點空間消耗換得極高的時間提速是非常有意義的,這種思想也是很重要的。
下面還有更厲害的,我們一起來分析具體的代碼。

求next數組的代碼

下面我們一起來欣賞計算機如何求得next數組的

typedef struct
{	char data[MaxSize];int length;			//串長
} SqString;
//SqString 是串的數據結構
//typedef重命名結構體變量,可以用SqString t定義一個結構體。
void GetNext(SqString t,int next[])		//由模式串t求出next值
{int j,k;j=0;k=-1;next[0]=-1;//第一個字符前無字符串,給值-1while (j<t.length-1) //因為next數組中j最大為t.length-1,而每一步next數組賦值都是在j++之后//所以最后一次經過while循環時j為t.length-2{	if (k==-1 || t.data[j]==t.data[k]) 	//k為-1或比較的字符相等時{	j++;k++;next[j]=k;//對應字符匹配情況下,s與t指向同步后移//通過字符串"aaaaab"求next數組過程想一下這一步的意義//printf("(1) j=%d,k=%d,next[%d]=%d\n",j,k,j,k);}else{k=next[k];**//我們現在知道next[k]的值代表的是下標為k的字符前面的字符串最長相等前后綴的長度//也表示該處字符不匹配時應該回溯到的字符的下標//這個值給k后又進行while循環判斷,此時t.data[k]即指最長相等前綴后一個字符**//為什么要回退此處進行比較,我們往下接著看。其實原理和上面介紹的KMP原理差不多//printf("(2) k=%d\n",k);}}
}

解釋next數組構造過程中的回溯問題

大家來看下面的圖:
下面的長條代表子串,紅色部分代表當前匹配上的最長相等前后綴,藍色部分代表t.data[j]。
在這里插入圖片描述在這里插入圖片描述

KMP算法代碼解釋

int KMPIndex(SqString s,SqString t)  //KMP算法
{int next[MaxSize],i=0,j=0;GetNext(t,next);while (i<s.length && j<t.length) {if (j==-1 || s.data[i]==t.data[j]) {i++;j++;  			//i,j各增1}else j=next[j]; 		//i不變,j后退,現在知道為什么這樣讓子串回退了吧}if (j>=t.length)return(i-t.length);  	//返回匹配模式串的首字符下標else  return(-1);        		//返回不匹配標志
}

KMP算法的改進

為什么KMP算法這么強大了還需要改進呢?
大家來看一個例子:
主串s=“aaaaabaaaaac”
子串t=“aaaaac”
這個例子中當‘b’與‘c’不匹配時應該‘b’與’c’前一位的‘a’比,這顯然是不匹配的。'c’前的’a’回溯后的字符依然是‘a’。
我們知道沒有必要再將‘b’與‘a’比對了,因為回溯后的字符和原字符是相同的,原字符不匹配,回溯后的字符自然不可能匹配。但是KMP算法中依然會將‘b’與回溯到的‘a’進行比對。這就是我們可以改進的地方了。我們改進后的next數組命名為:nextval數組。KMP算法的改進可以簡述為: 如果a位字符與它next值指向的b位字符相等,則該a位的nextval就指向b位的nextval值,如果不等,則該a位的nextval值就是它自己a位的next值。 這應該是最淺顯的解釋了。如字符串"ababaaab"的next數組以及nextval數組分別為:

下標01234567
子串ababaaab
next-10012311
nextval-10-10-1310

我們來分析下代碼:

void GetNextval(SqString t,int nextval[])  
//由模式串t求出nextval值
{int j=0,k=-1;nextval[0]=-1;while (j<t.length) {if (k==-1 || t.data[j]==t.data[k]) {	j++;k++;if (t.data[j]!=t.data[k]) 
//這里的t.data[k]是t.data[j]處字符不匹配而會回溯到的字符
//為什么?因為沒有這處if判斷的話,此處代碼是next[j]=k;
//next[j]不就是t.data[j]不匹配時應該回溯到的字符位置嘛nextval[j]=k;else  nextval[j]=nextval[k];
//這一個代碼含義是不是呼之欲出了?
//此時nextval[j]的值就是就是t.data[j]不匹配時應該回溯到的字符的nextval值
//用較為粗鄙語言表訴:即字符不匹配時回溯兩層后對應的字符下標}else  k=nextval[k];    	}}

int KMPIndex1(SqString s,SqString t)    
//修正的KMP算法
//只是next換成了nextval
{int nextval[MaxSize],i=0,j=0;GetNextval(t,nextval);while (i<s.length && j<t.length) {if (j==-1 || s.data[i]==t.data[j]) {	i++;j++;	}else j=nextval[j];}if (j>=t.length)  return(i-t.length);elsereturn(-1);
}

剩下的話

在寫這篇博客時,我想起了編譯原理老師講過的一些知識,其實KMP算法的步驟與自動機有不少相似之處,有興趣的朋友不妨聯系對比一下。

參考書籍

《數據結構教程》(第5版) 李春葆
《大話數據結構》 程杰


---------------------
作者:哈頓之光
來源:CSDN
原文:https://blog.csdn.net/weixin_46007276/article/details/104372119
版權聲明:本文為作者原創文章,轉載請附上博文鏈接!
內容解析By:CSDN,CNBLOG博客文章一鍵轉載插件

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

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

相關文章

BGP-MED-2

BGP-MED-2如圖&#xff1a;當AS100去往AS300的60、10的網絡時&#xff0c;60走R3&#xff0c;10走R1!使用MED屬性影響選路&#xff01; R2的配置 bgp 200peer 1.1.1.1 as-number 100 peer 1.1.1.1 ebgp-max-hop 255 peer 1.1.1.1 connect-interface LoopBack0peer 4.4.4.4 as-n…

WPF 實現 Gitee 氣泡菜單(一)

WPF 實現 Gitee 氣泡菜單&#xff08;一&#xff09;氣泡菜單&#xff08;一&#xff09;作者&#xff1a;WPFDevelopersOrg原文鏈接&#xff1a; https://github.com/WPFDevelopersOrg/WPFDevelopers框架使用大于等于.NET40&#xff1b;Visual Studio 2022;項目使用 MIT 開…

[轉]LVS負載均衡(LVS簡介、三種工作模式、十種調度算法)

一、LVS簡介 LVS&#xff08;Linux Virtual Server&#xff09;即Linux虛擬服務器&#xff0c;是由章文嵩博士主導的開源負載均衡項目&#xff0c;目前LVS已經被集成到Linux內核模塊中。該項目在Linux內核中實現了基于IP的數據請求負載均衡調度方案&#xff0c;其體系結構如圖1…

一張圖看懂微軟Power BI系列組件

一、Power BI簡介 Power BI是微軟最新的商業智能&#xff08;BI&#xff09;概念&#xff0c;它包含了一系列的組件和工具。話不多說&#xff0c;直接上圖吧&#xff1a; Power BI的核心理念就是讓我們用戶不需要強大的技術背景&#xff0c;只需要掌握Excel這樣簡單的工具就能快…

互聯網項目總結

2019獨角獸企業重金招聘Python工程師標準>>> 從去年年底開始專門被分配到互聯網小組做項目&#xff0c;一直想做個總結&#xff0c;但是苦于太貪玩。好吧&#xff0c;借著小組技術交流來一發。這里只對自己新學習的技術或者一些小技巧做簡要概述&#xff0c;不做深究…

【ArcGIS微課1000例】0036:分式標注案例教程

【拓展閱讀】:【ArcGIS Pro微課1000例】0015:ArcGIS Pro中屬性字段分式標注案例教程 文章目錄 1. 符號化2. 分式標注1. 符號化 右鍵數據圖層→符號系統,打開符號系統對話框,住符號系統選擇【唯一值】,字段1選擇NAME。 唯一值標注效果: 2. 分式標注 雙擊打開圖層屬性,切…

【轉】 ConstraintLayout 完全解析 快來優化你的布局吧

轉自&#xff1a; http://blog.csdn.net/lmj623565791/article/details/78011599 本文出自張鴻洋的博客 一、概述 ConstraintLayout出現有一段時間了&#xff0c;不過一直沒有特別去關注&#xff0c;也多多少少看了一些文字介紹&#xff0c;多數都是對使用可視化布局拖拽&#…

IoTDB 的C# 客戶端發布 0.13.0.7

IoTDB C# Client 0.13.0.7 已經發布&#xff0c; 此版本更新的內容為筆者為Apache-IoTDB-Client-CSharp實現了Ado.Net的兼容層&#xff0c;降低了對IoTDB的使用門檻。于此同時&#xff0c; IoTSharp也開始支持了IoTDB的數據入庫&#xff0c;隨著晚些時候IoTSharp 2.7 版本的發布…

[轉]Docker超詳細基礎教程,快速入門docker

一、docker概述 1.什么是docker Docker 是一個開源的應用容器引擎&#xff0c;基于 Go 語言 并遵從 Apache2.0 協議開源。 Docker 可以讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中&#xff0c;然后發布到任何流行的 Linux 機器上&#xff0c;也可以實現虛擬…

【Zookeeper】源碼分析之服務器(一)

一、前言 前面已經介紹了Zookeeper中Leader選舉的具體流程&#xff0c;接著來學習Zookeeper中的各種服務器。 二、總體框架圖 對于服務器&#xff0c;其框架圖如下圖所示 說明&#xff1a; ZooKeeperServer&#xff0c;為所有服務器的父類&#xff0c;其請求處理鏈為PrepReques…

linux下配置samba服務器(以CentOS6.7為例)

一、簡介&#xff08;百度百科&#xff09;Samba是在Linux和UNIX系統上實現SMB協議的一個免費軟件&#xff0c;由服務器及客戶端程序構成。SMB&#xff08;Server Messages Block&#xff0c;信息服務塊&#xff09;是一種在局域網上共享文件和打印機的一種通信協議&#xff0c…

【ArcGIS微課1000例】0037:上下標標注記案例教程

在利用ArcGIS進行制圖時&#xff0c;進行標注(Label) 或注記(Annolation) 是必不可少的。但是除了常規的標注和注記以外&#xff0c;還時常需要一些特殊的標注或注記&#xff0c;比如上標、下標等。 文章目錄一、上標標注方法二、下標標注方法一、上標標注方法 上下標代碼模板…

Redis——緩存擊穿、穿透、雪崩

1、緩存穿透&#xff1a; &#xff08;1&#xff09;問題描述&#xff1a;key對應的數據并不存在&#xff0c;每次請求訪問key時&#xff0c;緩存中查找不到&#xff0c;請求都會直接訪問到數據庫中去&#xff0c;請求量超出數據庫時&#xff0c;便會導致數據庫崩潰。如一個用…

數據庫性能系列之子查詢

前言說起數據庫&#xff0c;想必一些朋友會認為&#xff0c;數據庫不就是天天CRUD嗎&#xff1f;只要我掌握了這幾招&#xff0c;根本不在話下。是的&#xff0c;其實我也很贊同這個觀點&#xff0c;對于大多數應用程序來說&#xff0c;只掌握這些內容&#xff0c;是可以勝任日…

shell printf命令:格式化輸出語句

shell printf命令&#xff1a;格式化輸出語句注意&#xff1a;使用printf的腳本比使用echo移植性好。如同echo命令&#xff0c;printf命令可以輸出簡單的字符串&#xff1a;[rootmaster ~]#printf "Hello, Shell\n"Hello, Shellprintf不像echo那樣會自動提供一個換行…

UVa 10905 孩子們的游戲

https://vjudge.net/problem/UVA-10905 題意&#xff1a; 給定n個正整數&#xff0c;把它們連接成一個最大的整數。 思路&#xff1a; 實在是沒想到直接用string來排序了。 1 #include<iostream> 2 #include<algorithm>3 #include<string>4 using namespace …

laravel 內部驗證碼

為什么80%的碼農都做不了架構師&#xff1f;>>> 1.找到此文件composer.json 如下圖添加 "gregwar/captcha": "1.*" 行代碼 2.在命令行中執行 composer update 安裝完成后 3.找到控制器添加如下代碼 public function captcha($tmp) {//生成驗證…

k8s docker集群搭建

一、Kubernetes系列之介紹篇 1.背景介紹 云計算飛速發展 - IaaS - PaaS - SaaS Docker技術突飛猛進 - 一次構建&#xff0c;到處運行 - 容器的快速輕量 - 完整的生態環境 2.什么是kubernetes 首先&#xff0c;他是一個全新的基于容器技術的分布式架構領先方案。Kubernetes(k8…

leetcode 66 Plus One

給定一個數組&#xff0c;表示整數的各個位數&#xff0c;現要將其加上1&#xff0c;考慮進位。 vector<int> plusOne(vector<int>& digits) {int size digits.size();bool carry true;for (int i size - 1; i > 0; --i) {if (digits[i] 9 && c…

如何讓最小 API 綁定查詢字符串中的數組

前言在上次的文章中&#xff0c;我們實現了《讓 ASP.NET Core 支持綁定查詢字符串中的數組》&#xff1a;[HttpGet] public string Get([FromQuery][ModelBinder(BinderType typeof(IntArrayModelBinder))] int[] values) {return string.Join(" ", values.Select(p…