串的应用与kmp算法讲解

1. 写作目的

平时学习总结的学习笔记,方便自己理解加深印象。同时希望可以帮到正在学习这方面知识的同学,可以相互学习。新手上路请多关照,如果问题还请不吝赐教。

----------

2. 串的逻辑存储

       串指的是字符串,是一种特殊的线性表,特殊性在于只能存储字符,即可以使用顺序存储也可以使用链式存储,简单的谈一下两种存储结构的优缺点。

顺序存储

       顺序存储使用的是数组,既然是数组就是申请固定空间,当串需要拼接,替换时,可能会对数组进行扩容,这种操作就比较耗时,而且有时数组空间利用率很低,也浪费了一部分空间。但是优点也是显而易见的,定位速度快。

链式存储

       链式存储不拘束于空间大小,需要多少空间就链上多少个节点。但是如果每个节点都只存一个字符,那么无疑是浪费了空间,如果存多个字符,那么在字符查找上需要花费更多的时间,而且链表本身查找速度慢。

我的观点

       更倾向于顺序存储,毕竟字符串更为频繁的操作是查找功能,相较于链式存储,牺牲一点空间,换取更快查询的速度。


3. 串的应用--暴力查找

       下面说说串的应用:子串的查找。这个应该很熟悉,有好多应用场景是希望在一个主串中找到子串。可能想找到子串的位置,也可能想判断是否存在,或者替换,全部替换等,这就需要我们能完成最基本的操作,找出子串。

       下面这个例子可能是我们最容易想到的算法,直接找:两个循环嵌套,外层循环(i)逐一遍历主串每个字符,判断是否能和子串首字母匹配,如果匹配就继续判断第二个字符,直到把子串遍历完,就是找到了。但是如果中途某个字符不匹配,就把 i 回溯,回到匹配最初的地方,来进行下一个字符的首字母匹配。基本过程如下:

`public static int matchPattern(String origin,String aim) {

	char[] origins = origin.toCharArray();
char[] aims = aim.toCharArray(); for(int i = 0; i < origins.length; i++) { int j = 0,k = i;
for( ; j < aims.length;k++,j++ ) { if(origins[k] != aims[j]) {
break;
}
}
if(j == aims.length) {
return i - j;
}
}
return -1;
}`

       这个方法应该都可以想到,但是这样写会有问题。问题就是:

1.

2.





3.



4.



5.

       似乎感觉很正常,但如果仔细查看会发现,每当快要匹配成功的时候,因为一个字符的不匹配,就要回头再来,如果字符串很长,而目标字符相似度很高,就会一直重复这样的比较,重要的是其实我们可以根据已知的比较结果来减少比较的次数,来优化这种算法。


4. KMP算法

       简单说一下这个算法的背景,KMP的由来。

       KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现,因此人们称它为克努特——莫里斯——普拉特操作(简称KMP算法)。

       用我自己的话概述就是在匹配字符串过程中,因为某个字符匹配失败后,根据已有匹配成功的串推算出下次子串需要移动的位置,从而达到减少不必要匹配次数,实现快速匹配。那么重点就来了,如何推算出子串需要移动的位置? 我怎么知道这样移动后能一定能保证不会有遗漏。那下面我们就一起来分析一下(还是使用上面的例子,为了描述方便,我们把主串称为o(origin),子串称为a(aim) )。

  1. 第一步这样匹配没问题。

  2. 第二步这样匹配就出现问题了。 问题是上一步 i 已经移动到 4(数组下标),就因为o[4] != a[4],本次匹配失败,i 需要回溯到 1,j 需要回溯到 0 ,之前的匹配结果信息根本没有派上用场。



    因为根据已经匹配信息,o[0] = a[0],o[1] = a[1],o[2] = a[2],o[3] = a[3],前4位是对应相等的,而且o[2] = a[0],o[3] = a[1],我们完全可以这样移动:



    这样的话可以不用比较o[1]与a[0],o[2]与a[0],o[3]与a[1],直接比较o[4]与a[2],省去了不必要的步骤。那么我们继续往下分析,显然o[4] != a[2],因为o[2] = a[0],o[3] = a[1],而且a[0],a[1]不相等,我们可以推断出,o[3] != a[0],所以这一比较步骤也可以省略,直接比较a[4]与a[0]。最终得到结果



    这样的比较方式,是我们可以接受的,根据已知匹配信息省略了许多比较操作,提高效率。

  3. 那可能有人就要问了,你是怎么知道确切的移动位置,可以让 j 定位到合适的位置,这也就引出了KMP算法的核心的部分,求next[]。next[]有什么作用,next[0],next[1]....next[n]的含义是什么?next是存放对应子串字符冲突后,需要移动 j 的位置。举个例子:next[4] = 2,意思是在 j = 4的位置上子串和主串不匹配,那么主串 i 不需要回溯,直接把 j 回溯到 2,进行比较,也就是把next[]的每个值都求出来,我们就可以轻松准确的更改 j 的位置。了解了next[]的用途,下面我们来学习next[]是如何求出来的。

  4. 拿之前的例子作为说明:



    o[4]与a[4]冲突,前4位对应相等(首要条件),我们要是想根据前4位相等来推算位置,应该会出现2中情况。一:子串前4位互不相等(a[0],a[1],a[2],a[3]没有一个相同的),根据以上2个条件(o,a对应相等),直接可以推算,a[0]下次应该直接和o[4]比,原因就是a[0]不会和o前4位任意字符相等。 二:子串前4位有字符相等,而且是前后缀对应相等,也是可以推算位置。 插播一下前后缀知识。

    例如一串字符 "abadaba"

           前缀:{"a","ab","aba","abad","abada","abadab"}

           后缀:{"a","ba","aba","daba","adaba","badaba"}

    那么前后缀最长匹配相等串"adaba"。

    回到刚才的问题,对于串"abab",最长匹配相等串为"ab"。那么我们是可以这样理解的:



    我们分析的是a的前后缀关系,但前提提交是o,a对应位置相等,所以a的前缀就对应了o的后缀,这样我们可以直接移动 j 2的位置。理论基本就这些,我们通过代码来实现一下:

     public static int[] getNext(String aim,int length) {
    char[] chars = aim.toCharArray(); int i = 0,j = -1; //next数组长度和aim长度是相等的,因为每一个字符比较出冲突都需要有对应j的位置
    int[] next = new int[length]; //第0个位置前是没有前后缀的,所以next[0]我们规定为 -1.
    next[0] = -1;
    //第1个位置不匹配,前面只有一个字符,我们只能把j移动到0.
    next[1] = 0; //注意:这里 i < length -1,而非是 i < length。原因就是 i在循环内部先++,再赋值,如果是 < length会数组越界。
    while(i < length-1) { //判断如果对应字符相等,就累加前后缀相同字符长度
    if( j == -1 || chars[i] == chars[j] ) {
    next[++i] = ++j;
    }else {
    //注意:就这一句话,困扰我了2天,也许2是比较菜,不过我在文章中,重点解释一下,见文章。
    j = next[j];
    }
    }
    return next;
    }

重点来了

我们在求解next的时候并不需要o,只需要a就可以得出。我们只需要找出a中前后缀匹配长度,即可。那么我们的方法就是让a和a自己比较。比较的过程如下:

  • a[0]与a[1]比较,不相等

  • a[0]与a[2]比较,相等,next[ ++i ] = ++j, next[3] = 1。 意思是什么呢? 可以这样理解,在第3个字符前面的字符中,前后缀最大公共长度为1,也就是有1个公共字符,"a"。那么我知道这个信息有什么用处呢?用处就是当j在3位置上匹配失败,直接改变j为1来继续比较。

  • a[1]与a[3]比较,相等

  • a[2]与a[4]比较,不相等



    因为只是a[2]与a[4]不等,我们还不能确定具体的公共长度(next[4]的值),接下来我们需要回溯比较,a[4]与a[1],然后a[4]与a[0],如果这样比较多话,就又成了普通的暴力比较 ,我们可以根据已有的结果来推算。下面我来说一下困惑我2天的问题,j = next[ j ]

    现有我们已经推算的结果:next[ 0 ] = -1, next[ 1 ] = 0, next[ 2 ] = 0, next[ 3 ] = 1,next[ 4 ] = 2。按照算法来说,就是, j = next[2],j = 0。

           我之前一直认为 next[j] 的值代表的就是 j 需要移动的下标,所以很是不能理解把next[4]的值有放到next[]中(怎么能把下标放入到next[]中),也就是next[next[4]],next[4]很清晰的说明是当 j = 4 的位置有字符冲突时,把 j 移动到next[4],也就是2,那next[2]又是啥意思? 也就是实在搞不懂next[next[4]]。2天总是想着这个事,也继续在网上查阅资料,看有没有人和我同样有这个疑问。最后在不断的浏览中,感觉有可以说通的答案了,next[j] 也表示 0 ~ j-1 字符中前后缀最大长度,那么当next[4]失配后,next[4] = 2, 我们知道前面的4个字符,最大公共前后缀长度为2,那么next[2]就是在前2个字符当中再寻找最大前后缀公共的长度,因为是自身和自身比较,公共前后缀字符"ab"和 i=2 时前面的字符是一样的,所以在公共前后缀中再找相同前后缀即为在 i = 2之前找,也就是next[2]的值,相信到这里已经对这个疑问得到解答了。

next[ ]的求解我认为是kmp的核心,这个了解完之后,所剩内容不多。引用代码来使用next[ ]完成主串与模式串的查找吧。稍微改动了暴力求解的代码。

public static int kmp(String origin,String aim) {

	char[] origins = origin.toCharArray();
char[] aims = aim.toCharArray(); int[] next = getNext(aim,aim.length()); for(int i = 0; i < origins.length; i++) {
int j=0;
for( ; j < aims.length; ) { if(origins[i] != aims[j]) {
//改动
if(j == 0) {
break;
}
j = next[j];
}else {
i++;
j++;
}
}
if(j == aims.length) {
return i-j;
}
}
return -1;
}

5. 聊一下kmp的改进方案:

举例说明,有一种情况,我们的next[ ]有待优化,是这样一种情况。模式串:"aaaaaaab",显然我们的结果是next[]{0,0,1,2,3,4,5,6}。当和我们的主串"aaaaaaacaa....",会导致以下情况:

1.



2.



3.



4.



那我们如何改进呢?就是发现模式串中出现连续相等字符就让nextVal[ i ] = nextInt[ j ],解释一下就是: 回溯到上一个字符需要回溯的位置,因为2组前后缀对应想等,那就和上一个做相同处理。

public static int[] getNextVal(String aim,int length) {
char[] chars = aim.toCharArray(); int i = 0,j = -1; //next数组长度和aim长度是相等的,因为每一个字符比较出冲突都需要有对应j的位置
int[] nextVal = new int[length]; //第0个位置前是没有前后缀的,所以next[0]我们规定为 -1.
nextVal[0] = 0;
//第1个位置不匹配,前面只有一个字符,我们只能把j移动到0.
nextVal[1] = 0; //注意:这里 i < length -1,而非是 i < length。原因就是 i在循环内部先++,再赋值,如果是 < length会数组越界。
while(i < length-1) { //判断如果对应字符相等,就累加前后缀相同字符长度
if( j == -1 || chars[i] == chars[j] ) { i++;
j++;
//相较于getNext()改动的地方
if(chars[i] == chars[j]) { // i == j, i+1 == j+1
nextVal[i] = nextVal[j];
}else {
nextVal[i] = j;
} }else {
//注意:就这一句话,困扰我了2天,也许是比较菜,不过我在文章中,重点解释一下,见文章。
j = nextVal[j];
}
}
return nextVal;
}

参考文档:

《大话数据结构》

如何更好的理解和掌握 KMP 算法?

怎么理解kmp算法中的next数组?

从头到尾彻底理解KMP(2014年8月22日版)

串的应用与kmp算法讲解--学习笔记的更多相关文章

  1. 《数据结构》之串的模式匹配算法——KMP算法

    //串的模式匹配算法 //KMP算法,时间复杂度为O(n+m) #include <iostream> #include <string> #include <cstri ...

  2. 串的模式匹配和KMP算法

    在对字符串的操作中,我们经常要用到子串的查找功能,我们称子串为模式串,模式串在主串中的查找过程我们成为模式匹配,KMP算法就是一个高效的模式匹配算法.KMP算法是蛮力算法的一种改进,下面我们先来介绍蛮 ...

  3. KMP算法讲解

    老规矩,讲算法前,先说一道小问题吧 给你一个长串和短串,求短串在长串中出现的次数和位置. 设长串长度为len1,短串长度为len2. 如果len1*len2<=108,那就很简单了,直接暴力枚举 ...

  4. 串的模式之kmp算法实践题

    给定两个由英文字母组成的字符串 String 和 Pattern,要求找到 Pattern 在 String 中第一次出现的位置,并将此位置后的 String 的子串输出.如果找不到,则输出“Not ...

  5. 串的模式匹配,KMP算法

    串的模式匹配 现考虑一个常用操作,在字符串s(我们称为主串)中的第pos开始处往后查找,看在主串s中有没有和子串p相匹配的的,如果有,则返回字串p第一次出现的位置. 暴力求解 int Index(ch ...

  6. KMP字符串模式匹配学习笔记

    KMP算法实验 1.编程计算模式串(子串)的next值.2.利用KMP算法在主串中找到模式串的位置. 参考代码:---------int getNexlVal( char * s,  int j)// ...

  7. R︱shiny实现交互式界面布置与搭建(案例讲解+学习笔记)

    要学的东西太多,无笔记不能学~~ 欢迎关注公众号,一起分享学习笔记,记录每一颗"贝壳"~ --------------------------- 看了看往期的博客,这个话题竟然是第 ...

  8. 【KMP】【字符串】KMP字符串匹配算法 学习笔记

    一.简介     KMP是由Knuth.Morris和Prat发明的字符串匹配算法,它的时间复杂度是均摊\(O(n+m)\).其实用Hash也可以做到线性,只不过Hash存在极其微小的难以避免的冲突. ...

  9. KMP算法_读书笔记

    下面是KMP算法的实现伪代码: KMP_MATCHER ( T, P ) . n = T.length . m = P.length . next = COMPUTE_PREFIX_FUNCTION ...

随机推荐

  1. XShell 假死

    使用vim时因为使用windows word带来的坏习惯经常喜欢ctrl+s ,而这个造成的结果就是xshell假死,解决办法是ctrl+q

  2. 页面初次渲染loading图

    当第一次进入页面时,可能由于网速或其他原因请求接口需要等待很长时间,这是页面一片空白,很难看,切交互性也不好,这是,我们常常放上一个loading等待图给用户以反馈 // 页面尚未加载时的loadin ...

  3. Pandas中DataFrame数据合并、连接(concat、merge、join)之concat

    一.concat:沿着一条轴,将多个对象堆叠到一起 concat(objs, axis=0, join='outer', join_axes=None, ignore_index=False, key ...

  4. (转载) Consul 使用手册(感觉比较全了)

    使用consul 介绍 Consul包含多个组件,但是作为一个整体,为你的基础设施提供服务发现和服务配置的工具.他提供以下关键特性: 服务发现 Consul的客户端可用提供一个服务,比如 api 或者 ...

  5. JDK8日期处理API(转)

    转载地址:http://www.cnblogs.com/comeboo/p/5378922.html 转载地址:http://blog.csdn.net/hspingcc/article/detail ...

  6. UI案例

    <Window x:Class="WpfDemo2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/20 ...

  7. 主流包管理工具npm、yarn、cnpm、pnpm之间的区别与联系——原理篇

    接触 node 之后,一直使用npm包管理工具, cnpm 一开始会用一些,但是并没有觉得比 npm 快得多,使用 cnpm 的时候还经常安装不成功,只能再用 npm 安装一遍,渐渐的就弃用了 cnp ...

  8. 远程管理FTP

    FTP默认路径 建立pub目录(注意不是文件) LeapFTP使用 注:上传到服务器的pub文件下,不要弄错目录. 在本地计算机利用LeapFTP与FTPServer进行数据的传输,但是FTPServ ...

  9. python播放音乐

    最近一直想实现使用Python播放音乐的功能,找了百度上的好多博客,要不就只能播放wav格式的,要不播放mp3格式的但无法在Linux系统下使用的,或者只能在Python2的情况下播放的,写的都不符合 ...

  10. 文件操作(stat)

    /*** stat.c ***/ #include<stdio.h> #include<string.h> #include<sys/stat.h> #includ ...