七日打卡——数据结构与算法之KMP算法

2,401 阅读6分钟

数据结构与算法之KMP算法

前言

KMP算法是博主大学就接触过的线性时间字符串查找算法, 但是当时并没有特别的理解KMP算法,最近博主无意间看到了KMP算法,于是就拿来,嘿嘿嘿·······分享给大家。

定义

KMP算法是一种字符串查找的算法,用于在字符串S中查找出词W,以下是维基百科的KMP定义。

KMP算法(Knuth-Morris-Pratt字符串查找算法)
在计算机科学中,Knuth-Morris-Pratt字符串查找算法(简称为KMP算法)可在一个字符串S内查找一个词W的出现位置。一个词在不匹配时本身就包含足够的信息来确定下一个匹配可能的开始位置,此算法利用这一特性以避免重新检查先前匹配的字符。

根据KMP算法所说,我们可以简单抽象为以下题目:
给定一个字符串S和一个要查找的词W,求W在S中所在的位置。

算法前提

  1. 列出词W的所有子串(除单一元素除外)
  2. 列出词W每个子串对应的前缀与后缀(前缀:去除最后一个字符,剩下字符的子串;后缀:去除第一个字符,剩下字符的子串,前缀后缀去除掉单一元素)
  3. 根据前缀和后缀算出每个子串前缀与后缀的公共元素长度

算法思路

匹配过程中,需要使用两个指针i、j分别指向目前匹配到S的第i个字符与W的第j个字符(这里,注意j可能会被置为-1为了满足下述情况的)。 匹配有两种结果:

  1. j = -1 或者 S[i] = W[j]: i++, j++ ,进行S与W下一位的比较
  2. j != -1 或者 S[i] != W[j]:意味着在W的第j个位置字符串失配即不匹配,j = next[j]。
    • 2.1 next[j]不为-1,使得j移动到W的前j-1个字符的前缀后缀公共元素长度的位置(失配值),i保持不动。
    • 2.2 next[j]为-1,说明与W的第一个字符都不匹配,那么i++,j = 0

失配

在上面算法思路中,这里需要引入一个概念——部分匹配表,也称作失配函数,也就是上文中所做的算法前提的步骤,找寻到词W的各个子串前后缀公共元素长度。其实就是想知道字符串的从头开始的部分和到尾部结束的部分是否可以找到相同的两段,以及两段的长度。如W的某一子串"abbacdab",从头开始部分与到尾结束部分有着相同字段“ab”,这样在字符匹配时,如果W与S匹配到W这一子串结束后发生失配,那么W就可以跳过W的前两个字符串再进行与S匹配,因为S中必然是“abacdabXXX”,在下文会进行详细举例说明。

维基百科解释如下:
部分匹配表,又称为失配函数,作用是让算法无需多次匹配S中的任何字符。能够实现线性时间搜索的关键是在主串的一些字段中检查模式串的初始字段,我们可以确切地知道在当前位置之前的一个潜在匹配的位置。换句话说,在不错过任何潜在匹配的情况下,我们"预搜索"这个模式串本身并将其译成一个包含所有可能失配的位置对应可以绕过最多无效字符的列表。

KMP算法流程

下面就以维基百科所给例子来说明KMP算法的整个流程。
在这里插入图片描述

问题描述

已知字符串S="ABC ABCDAB ABCDABCDABDE",W="ABCDABD",设计一个线性时间匹配到W位置的算法。

问题分析

  1. 列出W的所有子串。 | W | | :-------: | | A | | AB | | ABC | | ABCD | | ABCDA | | ABCDAB | | ABCDABD |

  2. 列出每个子串的前缀后缀 以W的"ABCDAB"子串为例: | "ABCDAB"的前缀 | "ABCDAB"的后缀 | 该子串的前后缀公共长度 | | :-------: | :-------: | :----------------------: | | A | B | 0 | | AB | AB | 2 | | ABC | DAB | 2 | | ABCD | CDAB | 2 | | ABCDA | BCDAB | 2 |

当某一长度的前缀与后缀发生不同之后,即可停止计算剩余长度的前后缀公共长度。
3. 对于本案例的W,计算出如下失配数组
失配数组前后缀公共长度计算的基础上向右移一位,0位置移入-1.

4. 开始匹配

当匹配到i = j = 3时,W为'D'与S为' '不匹配,执行第二种结果,而W的子串"ABCD"的失配值为-1,那么根据算法思路2.2 可知此时需要才从W的0,S的i+1位置进行匹配.

当匹配到i = 10,j = 6时,W为'D'与S为' '不匹配,执行第二种结果,而W的子串"ABCDABD"的失配值为2,那么根据算法思路2.1 可知此时需要才从W的2,S的i+1位置进行匹配。这里借用维基百科的原话说明一下:我们可以注意到,"AB"在"ABCDAB"的头尾处均有出现。这意味着尾端的"AB"可以作为下次比较的起始点。

最后,当匹配到i 从15~21,j从0~6均匹配成功之后,返回i-j作为字符串与单词匹配成功的首字母位置。

算法代码

public class KMP {
    public static HashMap<Integer, Integer> next = new HashMap<>();
    public static int KMP_caluate(String S, String W) {
        int i = 0,j = 0;
        //前两项固定
        next.put(0,-1);
        next.put(1,0);

        //计算失配数组
        matchTable(W);
        System.out.println(next.toString());

        //开始匹配
        while (i < S.length() && j < W.length()) {
            char s = S.charAt(i);
            char w = W.charAt(j);
            if (s == w) {//匹配成功,下一位
                i++;
                j++;
            } else {
                j = next.get(j);//失败,获得失配位置
                if (j == -1) {//为-1,说明没有该字符匹配失败且无前缀后缀公共部分,S的下一位重新与W进行比较
                    i++;
                    j = 0;
                }
            }
        }
        return i - j;
    }

    public static void matchTable(String W) {
        for (int i = 2; i < W.length(); i++) {
            int len = 0;
            next.put(i, len);
            String subW = W.substring(0,i);
            for(int j = 1; j < i;j++) {
                String prefix = subW.substring(0,j);
                String suffix = subW.substring(i - j, i);
                if (prefix.equals(suffix)) {
                    len = j;
                    next.put(i,len);
                }
            }
        }
    }
    public static void main(String[] args) {
        String S = "acabaabaabcaccaabc", W ="abaabcac";
        int index = KMP_caluate(S,W);
        System.out.println(index);
    }
}

总结

KMP算法初步讲解就到这里结束了,理清楚字符串S和单词W的三种匹配思路相对来说就比较容易解决这一问题。镜头主要带大家梳理一下KMP算法的步骤流程,以及其内部的操作流程理解。