KMPBC:KMP算法及其改进(kmp with bad character)

前言

最近在看字符串匹配算法,突然灵光一闪有了想法,可以把kmp算法时间效率提高,同时保持最坏时间复杂度O(n+m)不变。其中n为主串长度,m为模式串长度,经测试可以块3-10倍,以为发现了新大陆,但是查阅文献后发现已经有了类似了改进。所以发表在CSDN上就算成功!

相关算法

  • KMP 第一个线性时间复杂度的串匹配算法。从左到右对模式串进行匹配,用i指针指向主串,j指针指向模式串。通过next数组快速回退指针j。
  • BM 可看作是KMP的改进,通右到左对模式串匹配,利用坏字符好后缀两个规则来向前移动指针i,大部分时候很快,但是最坏时间复杂度为O(nm)
  • Sunday 比BM更快,从左到右对模式串进行匹配,利用坏字符规则来移动i指针,但是最坏时间复杂度为O(nm)

算法过程

改进后的KMP算法如下:
首先计算next数组,按照kmp一般方法即可。
其次根据sunday算法计算shift数组。
然后用 i i i指向主串s某个字符,j指向模式p中的某个字符,从左到右进行匹配。

  • 如果i和j的字符相等,i,j两个指针同时向右移动。
  • 如果i和j的字符不同,则考虑使用坏字符规则,
    • 如果用坏字符规则能使i变大,则根据规则移动i, 并令j=0。
    • 否则,按照kmp的方法回退指针j。
public int match(String s, String p) {
    int n = s.length();
    int m = p.length();
    int[] next = buildNext(p);    
    int[] shift = buildShift(p);

    int i = 0;
    int j = 0;
    while(i < n && j < m){
        if(s.charAt(i) == p.charAt(j)){
            i++;           // #1
            j++;
        }else{             // #2
            int bad = i - j + m;
            if(bad < n){
              int nexti = bad - shift[s.charAt(bad)] + 1;
              if(nexti > i){
                i = nexti;
                j = 0;
                continue;
              }
            }
            if(j == 0) { // #3
                i++;
            }else{
                j = next[j-1];
            }
        }
    }
    return j == m ? i - m : -1;
  }

算法分析

如上一节代码:算法总体由if部分和else 两部分组成。
除了#3else部分位置无论是否使用坏字符规则都会对j进行回退(即将j减小)。我们考虑j的值是什么时候增加的,显然是在#1的时候。但#1#3执行次数加起来不会超过n, 因此j回退次数也不会超过n。

所以时间复杂度为 T ( n ) ≤ 2 n = O ( n ) T(n) \le 2n = O(n) T(n)2n=O(n)
如果加上预处理buildNext()buildShift()的话, T ( n ) = O ( n + m ) T(n)=O(n+m) T(n)=O(n+m)

实验对比

实验对KMP、BM、Sunday、KMPBC进行了比较,随机生成10000个字符串并随机生成它们的模式串。
四个算法某次运行结果如下,前四行展示了算法的运行时间,最后两个对比了Sunday和KMPBC的比较次数。
在这里插入图片描述
改进后的算法比较次数比sunday更少,且做到了理论上线性。

package org.example;

import java.util.*;

interface IStringMatch {
  int match(String s, String p);
}

class KMP implements IStringMatch {
  
  public static int[] buildNext(String p){
    int n = p.length();
    int[] next = new int[n];  // next[i] 表示 p[0..i] 最长共公前后缀和长度
    int pre = 0; //当前缀长度
    for(int i = 1; i < n; i++){
        if(p.charAt(i) == p.charAt(pre)){
            pre ++;
            next[i] = pre;
        }else{
            if(pre == 0) { // 防上pre-1溢出
                next[i] = 0;
            }else{
                pre = next[pre - 1]; // 在pre之间寻找更小的公共前后缀
                i--; 
            }     
        }
    }
    return next;
  }
  
  public int kmp(String s, String p){
    int n = s.length();
    int m = p.length();
    int[] next = buildNext(p);
    int i = 0;
    int j = 0;
    while(i < n && j < m){
        if(s.charAt(i) == p.charAt(j)){
            i++;
            j++;
        }else{
            if(j == 0) {  // 防上j-1溢出
                i++;
            }else{
                j = next[j-1];
            }
        }
    }
    return j == m ? i - m : -1;
  }

  @Override
  public int match(String s, String p) {
    return kmp(s, p);
  }
}

class BM implements IStringMatch {

  public static int[] buildShift(String p){
    int[] set = new int[256];
    for(int i = 0; i < p.length(); i++){
      set[p.charAt(i)] = (i + 1); // 记录从1开始的位置
    }
    return set;
  }


  @Override
  public int match(String s, String p) {

    // 1. 坏字符规则
    // 2. 好后缀规则
    // 这里直接引用第三方的实现:见附录
    return BMext.indexOf(s.toCharArray(), p.toCharArray());
  }
  
}

class Sunday implements IStringMatch {

  
  static int count = 0;

  @Override
  public int match(String s, String p) {
    int n = s.length();
    int m = p.length();  
    int[] shift = BM.buildShift(p);
    int i = 0;
    int j = 0;
    while(i < n && j < m){
      count ++;
      if(s.charAt(i) == p.charAt(j)){
        i ++;
        j ++;
      }else{
        int bad = i - j + m;
        if(bad < n){
          i += m - (shift[s.charAt(bad)] - 1);
          i -= j;
          j = 0;
        }else{
          return -1;
        }
      }
    }
    return j == m ? i - m : -1;
  }
  
}

class KMPWithBC implements IStringMatch {

  static int count = 0;
  
  @Override
  public int match(String s, String p) {
    int n = s.length();
    int m = p.length();
    int[] next = KMP.buildNext(p);    
    int[] shift = BM.buildShift(p);

    int i = 0;
    int j = 0;
    while(i < n && j < m){
        count ++;
        // 环字符规则加在这也行
        // int bad = i + (m - j) - 1;
        // if(bad < n && shift[s.charAt(bad)] == 0){
        //   i = bad + 1;
        //   j = 0;
        //   continue;
        // }
        if(s.charAt(i) == p.charAt(j)){
            i++;
            j++;
        }else{
            int bad2 = i - j + m;
            if(bad2 < n){
              int nexti = bad2 - shift[s.charAt(bad2)] + 1;
              if(nexti > i){
                i = nexti;
                j = 0;
                continue;
              }
            }
            if(j == 0) {  // 防上j-1溢出
                i++;
            }else{
                j = next[j-1];
            }
        }
    }
    return j == m ? i - m : -1;
  }

}


public class Main{

  
  static Random random=new Random();

  public static String getRandomString(int length){
    String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
      StringBuffer sb=new StringBuffer();
      for(int i=0;i<length;i++){
        int number=random.nextInt(5); // 62
        sb.append(str.charAt(number));
      }
      return sb.toString();
  }

  
  public static void main(String[] args){
    int n = 100000;
    String[] words = new String[n];
    String[] patts = new String[n];
    for(int i = 0; i < n; i++){
      String s = getRandomString(10000);
      words[i] = s;
      int pos = random.nextInt(s.length());
      int len = random.nextInt(128) + 1;
      patts[i] = s.substring(pos , Math.min(s.length(), pos + len));
    }

    // algorithms
    IStringMatch[] algs = new IStringMatch[]{
      new KMP(),
      new BM(),
      new Sunday(),
      new KMPWithBC()
    };
    // answers
    int[] ans = new int[n];
    for(int i = 0; i < n; i++){
      ans[i] = words[i].indexOf(patts[i]);
    }
    for(var al : algs){
      Date t1 = new Date();
      for(int i = 0; i < n; i++){
        int an = al.match(words[i], patts[i]);
        if(an != ans[i]) {
          System.out.println(al + ":" + words[i] + " matches " + patts[i] + " eqauls " + ans[i] + " but is" + an);
          System.exit(-1);
        }
      }
      Date t2 = new Date();
      System.out.println(al + ":" + ((t2.getTime() - t1.getTime())) + "ms");  
    }

    System.out.println("sunday: " + Sunday.count);
    System.out.println("kmpbc: " + KMPWithBC.count);
  }
}


附录

BM算法实现

package org.example;

class BMext {

      /**
     * Returns the index within this string of the first occurrence of the
     * specified substring. If it is not a substring, return -1.
     *
     * There is no Galil because it only generates one match.
     *
     * @param haystack The string to be scanned
     * @param needle The target string to search
     * @return The start index of the substring
     */
    public static int indexOf(char[] haystack, char[] needle) {
        if (needle.length == 0) {
            return 0;
        }
        int charTable[] = makeCharTable(needle);
        int offsetTable[] = makeOffsetTable(needle);
        for (int i = needle.length - 1, j; i < haystack.length;) {
            for (j = needle.length - 1; needle[j] == haystack[i]; --i, --j) {
                if (j == 0) {
                    return i;
                }
            }
            // i += needle.length - j; // For naive method
            i += Math.max(offsetTable[needle.length - 1 - j], charTable[haystack[i]]);
        }
        return -1;
    }

    /**
     * Makes the jump table based on the mismatched character information.
     */
    private static int[] makeCharTable(char[] needle) {
        //final int ALPHABET_SIZE = Character.MAX_VALUE + 1; // 65536
        final int ALPHABET_SIZE = 256;
        int[] table = new int[ALPHABET_SIZE];
        for (int i = 0; i < table.length; ++i) {
            table[i] = needle.length;
        }
        for (int i = 0; i < needle.length; ++i) {
            table[needle[i]] = needle.length - 1 - i;
        }
        return table;
    }

    /**
     * Makes the jump table based on the scan offset which mismatch occurs.
     * (bad-character rule).
     */
    private static int[] makeOffsetTable(char[] needle) {
        int[] table = new int[needle.length];
        int lastPrefixPosition = needle.length;
        for (int i = needle.length; i > 0; --i) {
            if (isPrefix(needle, i)) {
                lastPrefixPosition = i;
            }
            table[needle.length - i] = lastPrefixPosition - i + needle.length;
        }
        for (int i = 0; i < needle.length - 1; ++i) {
            int slen = suffixLength(needle, i);
            table[slen] = needle.length - 1 - i + slen;
        }
        return table;
    }

    /**
     * Is needle[p:end] a prefix of needle?
     */
    private static boolean isPrefix(char[] needle, int p) {
        for (int i = p, j = 0; i < needle.length; ++i, ++j) {
            if (needle[i] != needle[j]) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns the maximum length of the substring ends at p and is a suffix.
     * (good-suffix rule)
     */
    private static int suffixLength(char[] needle, int p) {
        int len = 0;
        for (int i = p, j = needle.length - 1;
                 i >= 0 && needle[i] == needle[j]; --i, --j) {
            len += 1;
        }
        return len;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/77576.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

内网ip与外网ip

一、关于IP地址 我们平时直接接触最多的是内网IP。而且还可以自己手动修改ip地址。而外网ip&#xff0c;我们很少直接接触&#xff0c;都是间接接触、因为外网ip一般都是运营商管理&#xff0c;而且是全球唯一的&#xff0c;一般我们自己是无法修改的。 内网IP和外网IP是指在…

【2024】MySQL中常用函数和窗口函数的基本使用方式

MySQL中常用函数和窗口函数的基本使用方式 一、基础函数1、聚合函数&#xff1a;2、字符串函数&#xff1a;3、日期和时间函数4、数值函数5、条件函数 二、窗口函数(*OVER*) 一、基础函数 1、聚合函数&#xff1a; SELECT COUNT(*) FROM table_name;&#xff1a;计算表中的行…

Effective C++学习笔记(8)

目录 条款49&#xff1a;了解new-handler的行为条款50&#xff1a;了解new和delete的合理替换时机条款51&#xff1a;编写new和delete时需固守常规条款52&#xff1a;写了placement new也要写placement delete条款53&#xff1a;不要轻忽编译器的警告条款54&#xff1a;让自己熟…

Spring Boot 中的 AOP,到底是 JDK 动态代理还是 Cglib 动态代理

大家都知道&#xff0c;AOP 底层是动态代理&#xff0c;而 Java 中的动态代理有两种实现方式&#xff1a; 基于 JDK 的动态代理 基于 Cglib 的动态代理 这两者最大的区别在于基于 JDK 的动态代理需要被代理的对象有接口&#xff0c;而基于 Cglib 的动态代理并不需要被代理对…

PyTorch训练简单的生成对抗网络GAN

文章目录 原理代码结果参考 原理 同时训练两个网络&#xff1a;辨别器Discriminator 和 生成器Generator Generator是 造假者&#xff0c;用来生成假数据。 Discriminator 是警察&#xff0c;尽可能的分辨出来哪些是造假的&#xff0c;哪些是真实的数据。 目的&#xff1a;使…

C++中List的实现

前言 数据结构中&#xff0c;我们了解到了链表&#xff0c;但是我们使用时需要自己去实现链表才能用&#xff0c;但是C出现了list将这一切皆变为现。list可以看作是一个带头双向循环的链表结构&#xff0c;并且可以在任意的正确范围内进行增删查改数据的容器。list容器一样也是…

【CSS】CSS 布局——常规流布局

<h1>基础文档流</h1><p>我是一个基本的块级元素。我的相邻块级元素在我的下方另起一行。</p><p>默认情况下&#xff0c;我们会占据父元素 100%的宽度&#xff0c;并且我们的高度与我们的子元素内容一样高。我们的总宽度和高度是我们的内容 内边距…

如何发布自己的小程序

小程序的基础内容组件 text&#xff1a; 文本支持长按选中的效果 <text selectable>151535313511</text> rich-text: 把HTML字符串渲染为对应的UI <rich-text nodes"<h1 stylecolor:red;>123</h1>"></rich-text> 小程序的…

2023牛客暑期多校训练营8-C Clamped Sequence II

2023牛客暑期多校训练营8-C Clamped Sequence II https://ac.nowcoder.com/acm/contest/57362/C 文章目录 2023牛客暑期多校训练营8-C Clamped Sequence II题意解题思路代码 题意 解题思路 先考虑不加紧密度的情况&#xff0c;要支持单点修改&#xff0c;整体查询&#xff0…

AUTOSAR NvM Block的三种类型

Native NVRAM block Native block是最基础的NvM Block&#xff0c;可以用来存储一个数据&#xff0c;可以配置长度、CRC等。 Redundant NVRAM block Redundant block就是在Native block的基础上再加一个冗余块&#xff0c;当Native block失效&#xff08;读取失败或CRC校验失…

时序预测 | MATLAB实现基于BiLSTM双向长短期记忆神经网络的时间序列预测-递归预测未来(多指标评价)

时序预测 | MATLAB实现基于BiLSTM双向长短期记忆神经网络的时间序列预测-递归预测未来(多指标评价) 目录 时序预测 | MATLAB实现基于BiLSTM双向长短期记忆神经网络的时间序列预测-递归预测未来(多指标评价)预测结果基本介绍程序设计参考资料 预测结果 基本介绍 Matlab实现BiLST…

2022年09月 C/C++(二级)真题解析#中国电子学会#全国青少年软件编程等级考试

第1题&#xff1a;统计误差范围内的数 统计一个整数序列中与指定数字m误差范围小于等于X的数的个数。 时间限制&#xff1a;5000 内存限制&#xff1a;65536 输入 输入包含三行&#xff1a; 第一行为N&#xff0c;表示整数序列的长度(N < 100); 第二行为N个整数&#xff0c;…

把握数据要素,做数字化时代的弄潮儿

截至2022年6月&#xff0c;我国网民规模已经达到了10.51亿&#xff0c;人均上网时间达到了每周29.5个小时&#xff0c;并且这部分人群使用手机上网的比例为99.6%。如果把工作、睡眠以及其他的必要的时间算上的话&#xff0c;可以发现通过手机上网已经成为了人们日常中的一部分。…

浅谈人工智能技术与物联网结合带来的好处

物联网是指通过互联网和各种技术将设备进行连接&#xff0c;实时采集数据、交互信息的网络&#xff0c;对设备实现智能化自动化感知、识别和控制&#xff0c;给人们带来便利。 人工智能是计算机科学的一个分支&#xff0c;旨在研究和开发能够模拟人类智能的技术和方法。人工智能…

SpringBoot的配置文件以及日志设置

在使用SpringBoot开发的过程中我们通常会用到配置文件来设置配置信息 以及使用日志来进行记录我们的操作&#xff0c;方便我们对错误的定位 配置文件的作用在于&#xff1a;设置端口&#xff0c;设置数据库连接信息&#xff0c;设置日志等等 在SpringBoot中&#xff0c;配置…

【LangChain概念】了解语言链️:第2部分

一、说明 在LangChain的帮助下创建LLM应用程序可以帮助我们轻松地链接所有内容。LangChain 是一个创新的框架&#xff0c;它正在彻底改变我们开发由语言模型驱动的应用程序的方式。通过结合先进的原则&#xff0c;LangChain正在重新定义通过传统API可以实现的极限。 在上一篇博…

SpringBoot携带Jre绿色部署项目

文章目录 SpringBoot携带Jre绿色部署运行项目1. 实现步骤2. 自测项目文件目录及bat文件内容&#xff0c;截图如下&#xff1a;2-1 项目文件夹列表&#xff1a;2-2. bat内容 3. 扩展&#xff1a; 1.6-1.8版本的jdk下载 SpringBoot携带Jre绿色部署运行项目 说明&#xff1a; 实…

【Python】Web学习笔记_flask(5)——会话cookie对象

HTTP是无状态协议&#xff0c;一次请求响应结束后&#xff0c;服务器不会留下对方信息&#xff0c;对于大部分web程序来说&#xff0c;是不方便的&#xff0c;所以有了cookie技术&#xff0c;通过在请求和响应保温中添加cookie数据来保存客户端的状态。 html代码&#xff1a; …

css鼠标样式 cursor: pointer

cursor: none; cursor:not-allowed; 禁止选择 user-select: none; pointer-events:none;禁止触发事件, 该样式会阻止默认事件的发生&#xff0c;但鼠标样式会变成箭头

什么文件传输协议才能保障跨国文件传输安全又稳定

在当今的全球化时代&#xff0c;跨国文件传输是一种常见而又重要的需求&#xff0c;无论是个人还是企业&#xff0c;都需要通过网络来分享和交换各种类型和大小的文件。但是&#xff0c;跨国文件传输也面临着许多挑战和风险&#xff0c;如何选择一个合适的文件传输协议&#xf…