KMP算法——寻找子串位置

KMP算法——寻找子串位置

1、KMP算法简介:

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度时间复杂度)O(m+n)。

2、什么是字符串匹配:

给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。

在Java中就是String的indexOf()方法。

3、对比暴力法和KMP算法:

首先我们可以比对一下暴力法和KMP算法的差距:

(1)暴力法

挨个截取遍历字符串,然后对比

image.png

(2)KMP算法

若遇到不匹配的,则会跳转到最小公共子串以后的地方进行匹配,会跳过中间不匹配的步骤,从而简化了时间复杂度。但是也都是空间换时间

image.png

这块声明一下,暴力法中的每一个字符匹配(连接的黑色剪头)是挨个匹配的,我是写到了一起,其实是很多步的;而KMP是直接跳转到最后一步的,跳过了公共子串的判断,可能是案例比较经典,跳转过来之后,就可以直接排除当前情况了。

感受玩KMP算法的魅力,现在开始操作起来,接收这愉快的“解题”环节(更像是受虐)。

4、KMP算法实现:

KMP算法一般分为两步:

  1. 提取加速匹配的信息(构造next数组),也是本算法中最难的部分。
  2. 字符串的匹配

4.1、提取加速匹配的信息(构造next数组)

KMP之所以可以跳过很多公共子串的匹配,是因为它的next数组里面记录了从当前下标位置,往前判断,找到最长公共子串的最后一个位置。

用数学语言表述:本文中的最小公共子串的概念,不是官方的定义,就自己读起来顺畅。

对于每模式串 t 的每个元素 t j,都存在一个实数 k ,使得模式串 t0 开头的 k 个字符
t_{0} \quad t_{1}\quad t_{2} \cdots t_{k-1}
依次与 t j 前面的 k
t_{j-k} \quad t_{j-k+1}\quad t_{j-k+2} \cdots t_{j-1}
,这里第一个字符 t j-k 最多从 t 1 开始,所以 k < j)个字符相同。如果这样的 k 有多个,则取最大的一个。

图解:

image.png

对于元素t5,存在一个实数2,使得模式串t0开头的2个字符 t0 t1,依次与t3和t4相同。

如果在匹配过程中,到t5的时候,和目标串不匹配了,此时就可以模式串回调到t2的位置,接着判断匹配。

因为t0 t1 是最大重复子串,所以就可以跳过判断,直接判断t2和当前目标字符串的字符是否匹配。

图解:

image.png

首先,先需要了解一下next数组,存放的是什么东西?

next数组其实就是存放着一个回调的位置,意思就是如果我匹配模式串的时候,假如到 t[i] 位置的时候不匹配了,那么我应该跳转到那个位置,接着对比匹配,可以跳过重复子串的判断。

例如上述案例,next[5] = 2

注意:next数组只和模式串有关系,和目标串没有关系。就算没有目标串,模式串也有next数组。

如果看到这块,你能够理解next的功能,恭喜你,你已经完成成功路程的一半了。

下来我们来一起探索一下,next数组应该如何得到?

首先,先拿到我们的模式串:我们的next数组的大小就是模式串pat的长度。

image.png

分为如下三种情况,来计算next的值:

我们用j来遍历模式串pat,初始值为0,用k作为临时变量,初始值为-1,来计算next[j]的值。

我们可以根据上面的理解可以直接写出来,next数组,但是,要怎样设置算法让计算机计算呢?

image.png

(1)特殊情况:

当j为0的时候,,next[0] = -1,目的是方便我们后面的计算。

当j为1的时候,pat[0]就是他的最小子串,所以next[1] = 0;

(2)当 pat[j] == pat[k] 或者 k == -1 的情况:

说明目前遍历到字符串为首元素,或者就是遍历到的字符和k所指向的位置元素相同,每次都是从字符串串首开始的,所以k的含义,不仅是记录下标位置,还有最长公共子串的长度。

这块的最长公共子串,是自己的定义,意思是pat[0] pat[1]…… pat[k] 字符串和 pat[j-k] pat[j-k+1]…… pat[j] 相同。

image.png

遇到这种情况,就让j++,k++,next[j] = k

这块注意,为什么我们判断的是pat[j] == pat[k],而要给next[j] = k++,因为我们next里面存放的是遇到不匹配的时候,要回调的位置,因为0到k的位置已经是匹配的了,所以我们直接跳转到k+1的位置,我们就给next[j] = k++。

(3)其他情况:也就是k的位置与j的位置不匹配

我们直到如果不匹配的话,就需要调用回调,让k的值回调。我们计算的next数组就是用来回调的。

k = next[k]

让k回调了之后,再次进入循环,若k的位置与j的位置还是不匹配,那么k就继续回调,直到k=-1,会满足第二种情况,即给next[j] = 0。

因为逻辑性很强,所以希望参考代码理解。

代码:

void Getnext(int next[],String pat)
{
    if(pat.length()==0) {
            next = new int[]{0};
            return;
    }
    next = new int[pat.length()];
    int j = 0, k = -1;
    next[0] = -1;
    while (j < pat.length() - 1) {
        if (k == -1 || pat.charAt(j) == pat.charAt(k)) {
            j++;
            k++;
            next[j] = k;     
        } else
            k = next[k];     //k回调
    }
}

4.2 字符串的匹配

当我们拿到next数组的时候,后面使用的时候会十分简单!!!

我们用 i 和 j 分别遍历 目标字符串s和模式串pat

我们这次只需要一个单层循环,这也是和暴力法降低时间复杂度最根本的地方。

单层循环:while( i<s.length()&&j<pat.length() )

  • s[i] == pat[j] 时,说明当前的字符匹配,i++;j++;
  • s[i] != pat[j] 时,说明当前的字符不匹配,让j进行回调,即 j = next[j];

不满足条件退出循环

  • j>=pat.length()时,说明已经找到模式匹配的子串了,直接return 找到的位置,即return i-pat.length();
  • 否则,说明没有找到模式匹配的子串,直接 return -1;

代码:

    public int indexOf(String s)
    {
        if(pat.length()==0) {
            return 0;
        }
        int i=0,j=0;

        while(i<s.length()&&j<pat.length())
        {
            if(j==-1 || s.charAt(i) == pat.charAt(j))
            {
                i++;
                j++;
            }
            else j=next[j];               //j回调
        }
        if(j>=pat.length())
            return (i-pat.length());         //匹配成功,返回子串的位置
        else
            return (-1);                  //没找到
    }

5、KMP完整代码:

    void Getnext(int next[], String pat) {
        next = new int[pat.length()];
        int j = 0, k = -1;
        next[0] = -1;
        while (j < pat.length() - 1) {
            if (k == -1 || pat.charAt(j) == pat.charAt(k)) {
                j++;
                k++;
                next[j] = k;
            } else
                k = next[k]; // k回调
        }
    }

    int indexOf(String s) {
        int i = 0, j = 0;

        while (i < s.length() && j < pat.length()) {
            if (j == -1 || s.charAt(i) == pat.charAt(j)) {
                i++;
                j++;
            } else
                j = next[j]; // j回调
        }
        if (j >= pat.length())
            return (i - pat.length()); // 匹配成功,返回子串的位置
        else
            return (-1); // 没找到
    }

6、利用面向对象封装

package com.company.project.arithmetic;

public class KMP {

    private int[] next;
    private String pat;

    /**
     * 在构造方法中计算next数组
     * @param pat 模式串
     */
    public KMP(String pat) {
        this.pat = pat;
        if(pat.length()==0) {
            next = new int[]{0};
            return;
        }
        next = new int[pat.length()];
        int j = 0, k = -1;
        next[0] = -1;
        while (j < pat.length() - 1) {
            if (k == -1 || pat.charAt(j) == pat.charAt(k)) {
                j++;
                k++;
                next[j] = k;
            } else
                k = next[k];
        }
        
    }

    /**
     * 寻找子串位置
     * @param s 目标串
     * @return
     */
    public int indexOf(String s) {
        if(pat.length()==0) {
            return 0;
        }
        int i = 0, j = 0;

        while (i < s.length() && j < pat.length()) {
            if (j == -1 || s.charAt(i) == pat.charAt(j)) {
                i++;
                j++;
            } else
                j = next[j]; // j回退。。。
        }
        if (j >= pat.length())
            return (i - pat.length()); // 匹配成功,返回子串的位置
        else
            return (-1); // 没找到
    }

    public static void main(String[] args) {
        KMP kmp = new KMP("");
        for (int i = 0; i < kmp.next.length; i++) {
            System.out.println(kmp.next[i]);
        }
        String s = "abababcabd";
        System.out.println("result:" + kmp.indexOf(s));
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容