Java代码实现KMP算法,找出字符串中所有匹配项的下标

274 阅读3分钟

最近在力扣上刷到一个题,如下:

  1. 找出字符串中第一个匹配项的下标

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。

示例 1:

输入:haystack = "sadbutsad", needle = "sad" 输出:0 解释:"sad" 在下标 0 和 6 处匹配。 第一个匹配项的下标是 0 ,所以返回 0 。 示例 2:

输入:haystack = "leetcode", needle = "leeto" 输出:-1 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。

提示:

1 <= haystack.length, needle.length <= 104 haystack 和 needle 仅由小写英文字符组成

猛一看,按照以往的编码习惯,那肯定是一顿indexOf输出呀,可这里要考查的点不会是问你会不会调用java的API那么简单,事实也确实没那么简单,没有头绪,只好先看题解,再去B站找大神的解题思路(文章末尾的相关的链接),再手算公共前后缀,最好再进入了编码练习,我这里把需求升级了一下,可以一次查找所有能匹配上的下标,下面把代码贴出来,记录一下。

使用KMP算法首先要求出“公共前后缀”数组,代码如下:

    /**
     * 获取公共前后缀长度数组
     * @param pattern   模式串数组
     * @param prefixTable   公共前后缀长度数组
     */
    public static void getPrefixTable(char[] pattern, int[] prefixTable){
        prefixTable[0] = 0; // 默认第0位为0
        int pLen = pattern.length;
        if (pLen > 1){
            int i = 1;      //模式串的下标,默认从下标为1的位置开始计算,因为前1位默认为0
            int len = 0;    //记录已经匹配到的公共前后缀的长度
            while(i < pLen){    //循环次数为模式串的长度
                if(pattern[i] == pattern[len]){ //当前循环到的字符,和上次匹配到公共前后缀长度的位置进行匹配,如果匹配到了
                    len++;  //则将公共前后缀的长度+1
                    prefixTable[i] = len;   //将prefixTable数组i位置的值赋值为连续匹配到的长度
                    i++;    //进入下一轮循环
                }else {     //本次循环没有匹配上
                    if (len > 0){   //判断上次匹配到的公共前后缀的长度,如果上次已匹配成功过,len长度肯定大于0
                        len = prefixTable[len - 1]; // 则将公共前后缀的长度回朔
                    }else {     //如果上次没有匹配到,本次也没有匹配到
                        prefixTable[i] = len;   //那么当前位置的公共前后缀长度就为0(此时的len为0,直接=0也是一样的)
                        i++;    //进入下一轮循环
                    }
                }
            }
        }
        prefixMove(prefixTable);
    }
 
    /**
     * 将公共前后缀数组整体向后移一位,首位设置为-1
     * @param prefixTable
     */
    public static void prefixMove(int[] prefixTable){
        int len = prefixTable.length;
        if (len > 1){
            for (int i = len - 1; i > 0; i--) {
                prefixTable[i] = prefixTable[i - 1];
            }
            prefixTable[0] = -1;
        }
    }

得到了公共前后缀长度数组之后,就可以进行匹配查找了

   /**
     * 在一个较长的字符串中找出目标字符串的位置
     * @param text  主串
     * @param pattern   模式串
     * @return  模式串在主串中的首个下标索引
     */
    public static int[] search(char[] text, char[] pattern){
        int[] indexResult = null;
        StringBuffer resultBuff = new StringBuffer();    //查找结果变量
        int m = text.length;    //主串的长度
        int n = pattern.length; //模式串的长度
        if(m > n){
            if(n == 0){
                indexResult = new int[]{-1};
                return indexResult;
            } else if (n == 1) {
                for (int i = 0; i < m; i++) {
                    if (text[i] == pattern[0]){
                        resultBuff.append(i); //将当前匹配到的索引记录下来
                        resultBuff.append(",");
                    }
                }
            }else {
                int i = 0;  //主串循环的索引
                int j = 0;  //模式串循环的索引
                int[] prefixTable = new int[n]; //存储公共前后缀的数组
                getPrefixTable(pattern, prefixTable);   //调用计算公共前后缀的方法
                while (i < m){  //遍历主串的长度
                    if((j == (n - 1)) && text[i] == pattern[j]){    //如果模式串遍历结束,且最后一位字符相等,即在主串中匹配到了一个完整的字符串
                        resultBuff.append((i - j)); //将当前匹配到的索引记录下来
                        resultBuff.append(",");
                        j = prefixTable[j]; //将模式串的下标移到公共前后缀指定的位置,继续匹配剩余的字符串
                    }
                    if(text[i] == pattern[j]){  //匹配到了一个字符,将主串和模式串的下标同时后移一位
                        i++;j++;
                    }else { //单个字符不匹配
                        j = prefixTable[j]; //将模式串的下标移到公共前后缀指定的位置,继续匹配
                        if(j == -1){    //如果模式串的下标为-1时,主串和模式串的下标同时后移一位
                            i++;j++;
                        }
                    }
                }
            }
            //将匹配结果转换为数组,并返回
            String strResult = resultBuff.toString();
            if (strResult.length() > 1){
                strResult = strResult.substring(0, strResult.length() - 1);
                String[] strIndex = strResult.split(",");
                int sLen = strIndex.length;
                indexResult = new int[sLen];
                for (int k = 0; k <sLen; k++) {
                    indexResult[k] = Integer.parseInt(strIndex[k]);
                }
            }
        }else {
            indexResult = new int[1];
            if(m == n){
                if(Arrays.equals(text, pattern)){
                    indexResult[0] = 0;
                }else {
                    indexResult[0] = -1;
                }
            } else if (m < n) {
                indexResult[0] = -1;
            }
        }
        return indexResult;
    }

最后调用main方法进行测试

    public static void main(String[] args) {
        String strText = "ababababcabaaabcacababcabaabbaabcababcabaaadsfbadsfababcabaadfasdfdasdfasdfasdfasdababcabaa";//主串
        String strPattern = "ababcabaa";    //模式串
//        String strText = "aabaaabaaac";//主串
//        String strPattern = "aabaaac";    //模式串
        char[] text = strText.toCharArray();
        char[] pattern = strPattern.toCharArray();
        int[] prefixTable = new int[pattern.length];
        int[] idxResult = search(text, pattern);
        System.out.println(Arrays.toString(idxResult));
 
//        getPrefix2(pattern, prefixTable);
//        System.out.println(Arrays.toString(prefixTable));
//        prefixMove(prefixTable);
//        System.out.println(Arrays.toString(prefixTable));
    }

这里要特别感谢B站一位名叫“正月点灯笼”的大神,感兴趣的可以去看一下他的解题思路:

【KMP字符串匹配算法1】

www.bilibili.com/video/BV1Px…