字符串模式匹配算法KMP

前言

给你两个字符串 haystack 和 needle ,在 haystack 字符串中找出 needle 字符串你会怎么做?

DF

使用简单粗暴的BF(Brute Force)算法,对主串haystack和模式串needle进行逐个字符的比较.
在这里插入图片描述

对比主串和模式串相应位置的值,相同则 i++, j++
在这里插入图片描述
对比主串和模式串相应位置的值,不相同则 i = i - j +1, j = 0

在这里插入图片描述

public static int bf(String ts, String ps) {
    
    
    char[] t = ts.toCharArray();
    char[] p = ps.toCharArray();
    int i = 0; // 主串的位置
    int j = 0; // 模式串的位置

    while (i < t.length && j < p.length) {
    
    
       if (t[i] == p[j]) {
    
     // 当两个字符相同,就比较下一个
           i++;
           j++;
       } else {
    
    
           i = i - j + 1; // 一旦不匹配,i后退
           j = 0; // j归0
       }
    }
    return j == p.length ? i - j : -1;
}

BM

如果碰到下图这情况,最后一位遇到不匹配字符时,BF算法就会右移一位模式串,再重新从头进行匹配。是否可以多移几位?在这里插入图片描述BM(Boyer-Moore)算法,是一种从右往左比较的算法。

坏字符规则

  1. 根据上图我们发现比较的第一个字符就不匹配,我们将主串这个字符称之为坏字符,也就是 f ,我们发现坏字符之后,模式串 T 中查找是否含有该字符(f),我们发现并不存在 f,此时我们只需将模式串右移到坏字符的后面一位即可。如下图

在这里插入图片描述
2. 我们继续从右往左进行比较,发现 d 为坏字符,则需要将模式串中最靠右的 d 和坏字符对齐
在这里插入图片描述

好后缀规则

在这里插入图片描述

  1. BM 算法是从右往左进行比较,发现坏字符的时候此时 cac 已经匹配成功,在红色阴影处发现坏字符。此时已经匹配成功的 cac 则为我们的好后缀,此时我们拿它在模式串中查找,如果找到了另一个和好后缀相匹配的串,那我们就将另一个和好后缀相匹配的串 ,滑到和好后缀对齐的位置。

在这里插入图片描述
2. 如果在模式串的头部没有发现好后缀,发现好后缀的子串也可以.

在这里插入图片描述

规则总结

好坏规则,都是针对碰到坏字符时,只对模式串进行处理,假设模式串为abcdab总结规则如下
在这里插入图片描述

    public int strStr(String haystack, String needle) {
    
    
        char[] hay = haystack.toCharArray();
        char[] need = needle.toCharArray();
        int haylen = haystack.length();
        int needlen = need.length;
        return bm(hay,haylen,need,needlen);
    }
 
    public static int bm (char[] a, int n, char[] b, int m) {
    
    

        int[] bc = new int[256];//创建一个数组用来保存最右边字符的下标
        badChar(b,m,bc);
        //用来保存各种长度好后缀的最右位置的数组
        int[] suffix_index = new int[m];
        //判断是否是头部,如果是头部则true
        boolean[] ispre = new boolean[m];
        goodSuffix(b,m,suffix_index,ispre);
        int i = 0;//第一个匹配字符
        //注意结束条件
        while (i <= n-m) {
    
    
            int j;
            //从后往前匹配,匹配失败,找到坏字符
            for (j = m - 1; j >= 0; --j) {
    
    
                if (a[i+j] != b[j]) break;
            }
            //模式串遍历完毕,匹配成功
            if (j < 0) {
    
    
                return i;
            }
            //下面为匹配失败时,如何处理
            //求出坏字符规则下移动的位数,就是我们坏字符下标减最右边的下标
            int x = j - bc[(int)a[i+j]];
            int y = 0;
            //好后缀情况,求出好后缀情况下的移动位数,如果不含有好后缀的话,则按照坏字符来
            if (y < m-1 && m - 1 - j > 0) {
    
    
                y = move(j, m, suffix_index,ispre);
            }
            //移动
            i = i + Math.max(x,y);

        }
        return -1;
    }
    // j代表坏字符的下标
    private static int move (int j, int m, int[] suffix_index, boolean[] ispre) {
    
    
        //好后缀长度
        int k = m - 1 - j;
        //如果含有长度为 k 的好后缀,返回移动位数,
        if (suffix_index[k] != -1) return j - suffix_index[k] + 1;
        //找头部为好后缀子串的最大长度,从长度最大的子串开始
        for (int r = j + 2; r <= m-1; ++r) {
    
    
            //如果是头部
            if (ispre[m-r] == true) {
    
    
                return r;
            }
        }
        //如果没有发现好后缀匹配的串,或者头部为好后缀子串,则移动到 m 位,也就是匹配串的长度
        return m;
    }
    
    //用来求坏字符情况下移动位数
    private static void badChar(char[] b, int m, int[] bc) {
    
    
        //初始化
        for (int i = 0; i < 256; ++i) {
    
    
            bc[i] = -1;
        }
        //m 代表模式串的长度,如果有两个 a,则后面那个会覆盖前面那个
        for (int i = 0; i < m; ++i) {
    
    
            int ascii = (int)b[i];
            bc[ascii] = i;//下标
        }
    }
    
    //用来求好后缀条件下的移动位数
    private static void goodSuffix (char[] b, int m, int[] suffix,boolean[] prefix) {
    
    
        //初始化
        for (int i = 0; i < m; ++i) {
    
    
            suffix[i] = -1;
            prefix[i] = false;
        }
        for (int i = 0; i < m - 1; ++i) {
    
    
            int j = i;
            int k = 0;
            while (j >= 0 && b[j] == b[m-1-k]) {
    
    
                --j;
                ++k;
                suffix[k] = j + 1;
            }
            if (j == -1) prefix[k] = true;
        }
    }
    

KMP算法(Knuth-Morris-Pratt)

BM显然是通过一定的规则,来减少移动次数及无谓的字符比较。KMP 算法的本质和BM是一样的.但规则更加简单。
在这里插入图片描述
红色阴影处匹配失败,绿色为匹配成功部分。在绿色部份找出最长公共前后缀
在这里插入图片描述
按绿色部份长度-最长公共前后缀长度移
在这里插入图片描述
KMP的核心原理已经基本搞定了,是不是比BM的规则简单,关键问题是怎么计算最长公共前后缀的长度? KMP 通过next数组提前计算
在这里插入图片描述

    public int strStr(String haystack, String needle) {
    
    
        //两种特殊情况
        if (needle.length() == 0) {
    
    
            return 0;
        }
        if (haystack.length() == 0) {
    
    
            return -1;
        }
        // char 数组
        char[] hasyarr = haystack.toCharArray();
        char[] nearr = needle.toCharArray();
        //长度
        int halen = hasyarr.length;
        int nelen = nearr.length;
        //返回下标
        return kmp(hasyarr,halen,nearr,nelen);
    }
    
    public int kmp (char[] hasyarr, int halen, char[] nearr, int nelen) {
    
    
        //获取next 数组
        int[] next = next(nearr,nelen);
        int j = 0;
        for (int i = 0; i < halen; ++i) {
    
    
            //发现不匹配的字符,然后根据 next 数组移动指针,移动到最大公共前后缀的,
            //前缀的后一位,和咱们移动模式串的含义相同
            while (j > 0 && hasyarr[i] != nearr[j]) {
    
    
                j = next[j - 1] + 1;
                //超出长度时,可以直接返回不存在
                if (nelen - j + i > halen) {
    
    
                    return -1;
                }
            }
            //如果相同就将指针同时后移一下,比较下个字符
            if (hasyarr[i] == nearr[j]) {
    
    
                ++j;
            }
            //遍历完整个模式串,返回模式串的起点下标
            if (j == nelen) {
    
    
                return i - nelen + 1;
            }
        }
        return -1;
    }
    //这一块比较难懂,不想看的同学可以忽略,了解大致含义即可,或者自己调试一下,看看运行情况
    //我会每一步都写上注释
    public  int[] next (char[] needle,int len) {
    
    
        //定义 next 数组
        int[] next = new int[len];
        // 初始化
        next[0] = -1;
        int k = -1;
        for (int i = 1; i < len; ++i) {
    
    
            //我们此时知道了 [0,i-1]的最长前后缀,但是k+1的指向的值和i不相同时,我们则需要回溯
            //因为 next[k]就时用来记录子串的最长公共前后缀的尾坐标(即长度)
            //就要找 k+1前一个元素在next数组里的值,即next[k+1]
            while (k != -1 && needle[k + 1] != needle[i]) {
    
    
                k = next[k];
            }
            // 相同情况,就是 k的下一位,和 i 相同时,此时我们已经知道 [0,i-1]的最长前后缀
            //然后 k - 1 又和 i 相同,最长前后缀加1,即可
            if (needle[k+1] == needle[i]) {
    
    
                ++k;
            }
            next[i] = k;

        }
        return next;
    }

主要参考

这可能是全网最细的 KMP 讲解

猜你喜欢

转载自blog.csdn.net/y3over/article/details/119617884