KMP模式匹配算法-串的应用

简介: KMP模式匹配算法-串的应用

前言

好久不见~各位看客老爷们,距离上次小向上班已经过去了好久--TT,小向也不想,但是被一个地方卡住了好久,最近才弄清楚。那么废话不多说,让我们进入今天的主题叭~数据结构之串及其应用KMP模式匹配算法。

还记得前段时间,小编参加英语考试,面对大量的生词,小编人都吓傻了,可是全部记忆一遍肯定来不及,记忆出现频率最高的肯定是效率最快的,这件事情说起来很简单,但是怎么才能知道什么单词出现的频率最高呢?这里面涉及到许许多多的东西,今天我们就不全部展开讲解,不过,这里面最重要的其实就是去找一个单词在一篇文章中的定位问题。即串的模式匹配。

微信图片_20220422154332.jpg

什么是串?

下面让我们来了解一下串。

虽然看到串的第一眼,大家可能有一点蒙的感觉,串?羊肉串?或者是别的balabala的东西。其实这里的串,指的是字符串。串:由零个或者多个字符组成的有限序列,又名叫字符串。一般我们把串记为s=”a1a2a3……an”,其中s是串的名称,用双引号或者单引号括起来的内容就是串的值,注意,在这个位置,双引号不是串的内容。打个比方说,《静夜思》“床前明月光,疑是地上霜。举头望明月,低头思故乡”,那么此时,《静夜思》就是串的名称,“床前明月光……”才是内容。微信图片_20220422154335.png零个字符是什么意思呢?就是啥也没有,这样的串,我们又称为空串,""直接这样表示就可以了。他的长度为0.在串的定义中我们可以发现,串是有顺序的,相邻的字符之间有前驱和后继的关系。并且串是有限的,一定要注意,串是有限的!

下面再让我们认识一个概念,主串,子串以及空格串。主串子串,其实很好理解,整个串的所有内容就是主串,而串中任意个数的连续字符组成的子序列称为该串的子串。那空格串就更好理解了,就是只包含有限个空格的串,记住是有限个空格,可以不是一个,但是不可能是无限个。现在再来看《静夜思》,我们就知道了,整首古诗就是一个主串,而里面的每个句子或者每连续的几个字,就是它的子串。

 大致清楚了串的一些定义之后,我们来了解一下串的比较大小方式。对于两个数来说,比较大小非常容易,2>1……。但是串怎么比较大小呢?其实串的比较大小方式大家猜应该也都可以猜出来,就是判断他们挨个字母的前后顺序。打一个比方来说,smart,stupid,他们的第一个字母都是s,认为不存在大小差异,而第二个字母,”m”字母在”t”字母之前,所以”m”<”t”.

但是事实上,对于计算机来说,他是不知道哪个字母在前哪个字母在后的,他是通过组成串的字符之间的编码来进行的。现代计算机一般都是通过ASCII编码,但是由于ASCII码是用7位二进制表示一个字符,总共只能表示128个字符,所以扩展ASCII码由8位二进制数表示一个字符,这样就可以表示256个字符,满足了大部分国家的需要。可是,对于我们国家那可是远远不够的,我们国家除了汉语,还有土家语,蒙古语……等等语言,所以就有了现在的Unicode编码,一般用16位的二进制数表示一个字符。

  那么现在我们就来正式的总结一下串的比较。

对于两个长度相等的串来说,必须每个相应位置的字符都相等,才算是相等。即a1=b1,a2=b2,……,an=bn.

 对于两个长度不相等的串来说,s1=”a1a2……an”,s2=”b1b2……bm”,

 如果n<m,且满足ai=bi,则更长的串更大。比如说,s1=“brain”,s2=”brainstorm”,就有s1<s2.

 对于n<m,但存在某个i,使得a1=a2……ai-1=bi-1,但是ai>bi,则s1>s2.

 大致的了解了串以后,本来是应该继续介绍串的抽象数据类型和储存结构,但是串的抽象数据类型和储存结构和栈类似,这里就不多加叙述了。下面就让我们进入串的应用部分,模式匹配算法。

朴素匹配算法


在刚开始的时候,我觉得写一个查找单词的程序很简单,就依次来比较就行了。过程在这里给大家进行简单的介绍。

 对于一个主串s=“annyainy”,我们需要查找子串t=””yibeizi”.我最开始的思路是依次进行匹配。

 主串s从第一位开始,s和t第一个字母不匹配,那么将s2和t1进行比较,依次类推,直到最后匹配成功。简单的说,就是对主串的每一个字符作为子串开头,与待匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或全部遍历完成为止。

 下面给出相应的代码


int index(string s, string t,int pos) 
{
  int i = pos;
  / pos是指开始搜索的位置,即开始搜索时的下标
  int j = 0;/ 这是当前子串的下标
  while(i < s.length() && t < b.length()) / 只有当i小于等于主串长度且j小于等于子串长度时进行循环 
  {
    if(s[i] == t[j]) 
    {
      ++i;
      ++t;
    } else 
    {
      i = i - j + 2 / i退回到上次匹配位置的下一位
      j = 1;
    }
    if(j = b.length())
    return i - b.length(); else
    return 0;
  }

那么现在一个简单的匹配代码我们已经写出来了,但是这是最简单的,也是最慢的。现在让我们来分析一下,如果每一次不成功的匹配都发生在串t的最后一个字符。举一个例子,s为“aaaaaaaaaaaab”,t为“aaaaaaab”,那么需要每次匹配到最后我们才知道,原来不匹配。这样的情况下,时间复杂度就是O((n-m+1)*m)。

 相信大家看到这样分析下来肯定会说,这确实太麻烦了,甚至难以忍受,那么就让我们来看看一种更高效的算法。由D.E.Knuth,J.H.Morris和V.R.Pratt发表的一个模式匹配算法,简称KMP算法。

KMP模式匹配算法



在最开始,我们先来看一个串,s=abcababcaaccda……,t=abcabz,他们在进行匹配的时候,匹配到第六位时发现不匹配,按照朴素匹配算法,他们会依次往前移动一位,再重新进行比较,即整个匹配过程我们是通过s的i的值的不断回溯来进行,但是,我们知道,t的第一位和s的第一位肯定不匹配,依次类推,直到和s的4位开始比较,才算步入正轨,那么我们可不可以把这些很显然不对的步骤删掉,把那些肯定匹配的步骤跳过呢?对,基于这种观点,大佬们提出了KMP算法来解决这些完全没有必要的回溯。

 如果i的值不回溯,也就是大小不能发生变化,那么要考虑的变化就是j的值了。通过对朴素匹配算法的观察,我们可以发现,要确定j值的变化,其实我们只要将当前j所在的位置和前面进行比较,如果出现了相同的字符,那么j的变化也会不一样,也就是说,j值的变化只取决于自身而不取决于主串。

 再拿上面的例子来说,t=abcdef,当中没有任何重复的字符,那么j就从6变为1,如果t=abcabz,当匹配到z的时候,发现不匹配,此时j就从6变为3,而不是1,因为前缀”ab”和z之前的后缀”ab”是相等的。由此我们就可以知道j的值取决于当前字符之前的串的前后缀的相似度。

如果把t串的各个位置的j值变化定义为一个数组next,那么数组next的长度就是t串的长度,于是我们就可以得到下面的函数定义。

微信图片_20220422154337.jpg


这里我们需要先明确一个概念,前缀和后缀。对于子串acesdz来说,当j=5时,他的去掉第五个字符的子串是“aces”,前缀就是去掉再最后一个字符“s”,依次的子串,即a,ac,ace,那么后缀也很清楚了,去掉最前面一个,即s,es,ces.


 那么next数组是怎么推导的呢?对于子串acesdz


1)当j=1时,next[1]=0.


2)当j=2时,j由1到j-1就只有字符“a”,属于其他情况next[2]=1.


3)当j=3时,j由1到j-1的串就是“ac”,显然“a”和”c”不等,属于其他情况,next[3]=1.


4)以此类推,最终next[j]为011111.


对于子串aecaex


1)当j=1时,next[1]=0.


2)当j=2时,next[2]=1.


3)当j=3时,next[3]=1.


4)当j=4时,next[4]=1.


5)当j=5时,next[5]=2.

void get_next(string t, int* next) 
{
  int i = 1;
  int j = 0;
  next[1] = 0;
  while(i < t[0]) 
  /*在这个位置t[0]表示t的长度*/ 
  {
    if(j == 0 || t[i] == t[j]) 
    {
      ++i;
      ++j;
      next[i] = j;
    } 
    else
      j = next[j];
  }
}

6)当j=6时,next[6]=3.


    我们根据经验就可以知道,如果前后缀一个字符相等,那么next[j]=1+1,n个字符相等就是next[j]=1+n.


    下面我们给出next数值推导的代码,


   下面给出具体的匹配过程的代码


/*此处s为主串,t为子串,pos为刚开始匹配的位置*/
int kmp(string s, string t, int pos) 
{
  int i = pos;
  int j = 1;
  int next[255];
  get_next(t, next);
  while(i <= s[0] && j <= t[0]) 
  {
    if(j == 0 || s[i] == t[j]) 
    {
      ++i;
      ++j;
    } else 
    {
      j = next[j];
    }
  }
  if(j > t[0])
    return i - t[0]; 
  else
    return 0;
}



    KMP算法的时间复杂度是O(n+m),相比较朴素匹配算法来说是快了很多,但是这种优势只体现在重复部分很多的情况,否则差异不明显。下面让我们来看看KMP算法的进一步优化。


KMP的改良

如果主串为s=aaaacde,子串为t=aaaaaz,next[j]=012345,i=5,j=5时,发现不匹配,此时j变为4,仍然不匹配,然后变成3,依然不匹配,后面肯定也是一样的不匹配,因为都是a,那么其实这些匹配都是不必要的,可以省去。那么我们完全可以用第一位的a的next数值来代替后面a的next数值,因此我们对next进行了改良。


void get_naxtval(string t, int* nextval) 
{
  int i = 1;
  int j = 0;
  nextval[1] = 0;
  while (i < t[0]) 
  {
    if (j == 0 || t[i] == t[j]) 
    {
      ++i;
      ++j;
      if (t[i] != t[j])
      nextval[i] = j; else
      nextval[i] = nextval[j];
    }
  }
}

 匹配算法和之前的是一样的这里就不再重复。这里nextval的推导就也不重复了,和next的推导过程大致相同。


KMP的再改良

   虽然介绍完了KMP算法的标准形式,但是,我发现在实际的操作中,有一些方面并不是很好操作,比如t[0],s[0]为字符串的长度,这里就需要进行一些别的操作实现,s[0],t[0]为字符串长度,麻烦了。在最后给出我在后面改进的KMP改良算法。希望大家在前面的内容看明白以后,来看看这个改良版本的算法。

#include<iostream>
#include<string>
using namespace std;
void get_nextval(string t, int* nextval) 
{
  int i = 1;
  int j = 0;
  int l = t.length();
  nextval[0] = -1;
  nextval[1] = 0;
  while (i <l) 
  {
    if (j==-1||t[i] == t[j]) 
    {
      ++i;
      ++j;
      if (t[i] != t[j])
      nextval[i] = j; else
      nextval[i] = nextval[j];
    } else
    j = nextval[j];
  }
}
int kmp(string s, string t, int pos) 
{
  int l1 = s.length();
  int l2 = t.length();
  int nextval[255];
  get_nextval(t, nextval);
  int i = pos;
  int j = 0;
  while (i < l1&&j<l2) 
  {
    if (j==-1||s[i] == t[j]) 
    {
      i++;
      j++;
    } else
    j = nextval[j];
  }
  if (j ==l2)
  return i - l2; else
  return -1;
}
void main() 
{
  string s, t;
  int pos;
  cout << "请输入主句"<<endl;
  cin >> s;
  cout << "请输入查找句"<<endl;
  cin >> t;
  cout << "请输入从哪一个字符开始查找(第一个字符的位置为0)"<<endl;
  cin >> pos;
  cout << "查找句位于" << kmp(s, t, pos);
}


微信图片_20220422154340.png



  快过年了,小编在这里给各位看客老爷们提前说一声,祝大家新年快乐!

相关文章
|
29天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
41 3
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习中的优化算法及其应用
【10月更文挑战第8天】 本文将探讨深度学习中常用的优化算法,包括梯度下降法、Adam和RMSProp等,介绍这些算法的基本原理与应用场景。通过实例分析,帮助读者更好地理解和应用这些优化算法,提高深度学习模型的训练效率与性能。
140 63
|
13天前
|
机器学习/深度学习 JSON 算法
二叉树遍历算法的应用场景有哪些?
【10月更文挑战第29天】二叉树遍历算法作为一种基础而重要的算法,在许多领域都有着不可或缺的应用,它为解决各种复杂的问题提供了有效的手段和思路。随着计算机科学的不断发展,二叉树遍历算法也在不断地被优化和扩展,以适应新的应用场景和需求。
23 0
|
24天前
|
存储 算法 搜索推荐
这些算法在实际应用中有哪些具体案例呢
【10月更文挑战第19天】这些算法在实际应用中有哪些具体案例呢
26 1
|
30天前
|
机器学习/深度学习 人工智能 算法
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
70 0
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
|
24天前
|
监控 算法 数据挖掘
HyperLogLog算法有哪些应用场景呢
【10月更文挑战第19天】HyperLogLog算法有哪些应用场景呢
15 0
|
30天前
|
机器学习/深度学习 算法 数据建模
计算机前沿技术-人工智能算法-生成对抗网络-算法原理及应用实践
计算机前沿技术-人工智能算法-生成对抗网络-算法原理及应用实践
25 0
|
1月前
|
前端开发 算法 JavaScript
无界SaaS模式深度解析:算力算法、链接力、数据确权制度
私域电商的无界SaaS模式涉及后端开发、前端开发、数据库设计、API接口、区块链技术、支付和身份验证系统等多个技术领域。本文通过简化框架和示例代码,指导如何将核心功能转化为技术实现,涵盖用户管理、企业店铺管理、数据流量管理等关键环节。
|
25天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
10天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。