在字符串中查找子字符串

今天中午一觉睡醒,刷b站,看见一个视频:

最浅显易懂的 KMP 算法讲解icon-default.png?t=M4ADhttps://www.bilibili.com/video/BV1AY4y157yL?spm_id_from=333.1007.top_right_bar_window_history.content.click

虽然他是用python写的,代码我只能看懂一点点,但是知道思想就能自己实现了。恰恰好,今天是周日,我这周的博客还没写,就来小小的码一下吧。

暴力解析

首先是暴力解析法,就是遍历源字符串的每一个字符,如果和目标字符串的首字符匹配了,就比较下一位字符,如果匹配到了就返回第一个字符的下标,如果没有匹配到就回到第一个字符的后一位继续比较,实现代码:

    /**
     * 暴力解析获取子串第一次出现的位置
     * @param src 源字符串
     * @param target 目标字符串
     * @return 如果目标串出现了返回目标串第一次出现的起始下标,目标串不存在返回-1
     */
    public static int violenceAnalysis(String src,String target){
        //目标字符串的长度
        int targetLength = target.length();
        //遍历源字符串,获取起始下标,当起始下标后的字符串长度比目标串短,就不可能匹配到了
        for (int srcIndex = 0; srcIndex < src.length() - targetLength + 1; srcIndex++){
            //如果首字符都不相同就不需要比较了
            if (src.charAt(srcIndex) != target.charAt(0)){
                continue;
            }
            //如果目标字符串长度为1,返回当前下标
            if (targetLength == 1){
                return srcIndex;
            }
            //临时下标,用于对比后面的字符是否相同
            int tempSrcIndex = srcIndex + 1;
            //对比目标字符串和源字符串后续字符
            for (int targetIndex = 1; targetIndex < targetLength; targetIndex++){
                //不相同就结束对比
                if (src.charAt(tempSrcIndex++) != target.charAt(targetIndex)){
                    break;
                }
                //对比到目标字符串最后一个字符,说明匹配到子串了
                if (targetIndex == targetLength - 1){
                    return srcIndex;
                }
            }
        }
        //结束对比还没有找到,返回-1
        return -1;
    }

这种方法需要不断回溯,非常耗时。

KMP

kmp算法需要一个next数组,这个数组记录了目标字符串target的每一位与开头相比相同的字符数,如“123123”的next数组是[0,0,0,1,2,3],"ACDABCAC"的next数组是[0,0,0,1,0,0,1,2]。

我们从头遍历源字符串src,如果与target的首字符相同就向后比较,在比较过程中如果发现不相同不需要跟暴力解析一样吧src的下标回溯到起始位置的下一位,只需要使target下标等于next数组中找到与target最后一位匹配上的字符串相同下标的数据,如target是"ACDABCAC"匹配到了B发现不同了,我们只需要将target下标赋值为1就行了。然后继续向后比较。说起来很复杂,直接上代码:

    /**
     * 获取next数组
     * @param target 需要获取next数组的字符串
     * @return 返回next数组
     */
    public static int[] getNextArray(String target){
        //res数组长度与target一样
        //每个位置记录的是target对应位置上字符与target开头匹配了几位
        int[] res = new int[target.length()];
        //第一个字符为0,因为没有字符跟它匹配
        res[0] = 0;
        for (int i = 1; i < target.length(); i++){
            //target.charAt(i)是当前字符
            //target.charAt(res[i - 1])是当前匹配到了开头往后第几个字符
            //如果相同,匹配成功,当前下标等于前一个下标+1
            if (target.charAt(i) == target.charAt(res[i - 1])){
                res[i] = res[i - 1] + 1;
            }else{
                //当前字符与首字符相同,重新开始皮皮额
                if (target.charAt(0) == target.charAt(i)){
                    res[i] = 1;
                }else{//与首字符不相同置为0
                    res[i] = 0;
                }
            }
        }
        return res;
    }

    /**
     * 使用kmp算法来查找目标串出现的位置
     * @param src 源串
     * @param target 目标串
     * @return 如果目标串存在返回第一次出现的位置,反之返回-1
     */
    public static int kmp(String src,String target){
        //获取next数组
        int[] next = getNextArray(target);
        //目标串下标
        int targetIndex = 0;
        //对比
        for (int srcIndex = 0; srcIndex < src.length(); srcIndex++){
            //如果匹配,目标串下标+1
            if (src.charAt(srcIndex) == target.charAt(targetIndex)){
                if (target.length() == 1){
                    return srcIndex;
                }
                targetIndex++;
                //如果下标大于目标串长度,匹配完成
                if (targetIndex == target.length()){
                    return srcIndex - target.length() + 1;
                }
            }else{
                //如果不匹配则目标串下标回溯到next数组中最后一个匹配字符对应的数
                targetIndex = next[targetIndex];
            }
        }
        //遍历结束都没有找到,返回-1
        return -1;
    }

这个算法只需要遍历一次src数组。

结果

测试一下,随机生成一个很长的字符串,然后在里面查找"1a1a1a1a":

    public static void main(String[] args) {
        Random random = new Random();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 0x11EFFFFF; i++){
            sb.append((char)(random.nextInt('z' - '0') + '0'));
        }
        String src = sb.toString();
        String target = "1a1a1a1a";

        long start1 = System.currentTimeMillis();
        System.out.println(violenceAnalysis(src,target));
        long end1 = System.currentTimeMillis();

        long start2 = System.currentTimeMillis();
        System.out.println(kmp(src,target));
        long end2 = System.currentTimeMillis();

        System.out.println("暴力解析用时:" + (end1 - start1));
        System.out.println("kmp用时:" + (end2 - start2));
    }

运行截图

在字符串中查找子字符串_第1张图片

在字符串中查找子字符串_第2张图片

 kmp确实比暴力解析要快

 

你可能感兴趣的:(java,算法)