Java 算法篇-深入了解 BF 与 KMP 算法

2024-04-19 19:12
文章标签 java 算法 深入 了解 kmp bf

本文主要是介绍Java 算法篇-深入了解 BF 与 KMP 算法,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

文章目录

        1.0 BF 算法概述

        1.1 BF 算法实际使用

        2.0 KMP 算法概述

        2.1 KMP 算法实际使用

        2.2 相比于 BF 算法实现,KMP 算法的重要思想

        2.3 为什么要这样设计?

        2.4 next 数组

        2.4.1 创建 next 数组原理

        2.4.2 创建 next 数组过程

        2.5 KMP 算法的实现


        1.0 BF 算法概述

        是一种基本的暴力搜索算法,也称为穷举算法或暴力匹配算法。BF 算法通过遍历所有可能的解空间来寻找问题的解,虽然效率较低,但在一些简单的问题上仍然具有一定的实用性。

        尽管 BF 算法效率较低,但在一些简单的问题上,它仍然可以提供可行的解决方案。在一些小规模的问题、教学示例或者需要快速验证解的情况下,BF 算法可以作为一种简单且直观的解决方法。

        1.1 BF 算法实际使用

        举个例子:用 BF 算法来找到主串 str 中是否存在子串 sub,如果存在,那么子串在主串的具体那个位置。

        实现思路:为了实现一个比较严谨的程序,首先对 str 与 sub 进行判断是否为 null 或者长度为 0 。

        接着,用变量 i 来记录主串 str 索引下标,用变量 j 来记录子串 sub 索引下标,且用 strLen 来记录主串的长度,用 sunLen 来记录子串的长度。

        再接着,用 while 循环,循环比较 str 与 sub 中字符是否相同,如 str.charAt(i) 与 sub.charAt(j) 进行比较,如果两者相同,那么继续往后走 i++ ,j++  ;如果两者不相同,那么对于主串来说,i 需要回到 i = i - j + 1 位置,对于 j 来说, 就要回到原点 j = 0 。

如图:

        最后,判断是什么原因导致跳出了循环:

        有两个原因:(1)j >= subLen ,则说明了 j 已经比较完毕了,所以主串中存在子串,位置位于:(i - j)。(2)i > strLen ,则说明,即使 i 都走完了, j 还没走完,那么主串中不存在该子串。

代码如下:

public class demo1 {//暴力解法public static void main(String[] args) {String str = "abbccccfffrreytur";String sub = "tu";bf(str,sub);}public static void bf(String str, String sub){if (str == null || sub == null){System.out.println("对象为 null");return;}if (str.length() == 0 || sub.length() == 0){System.out.println("长度不合法!!!!");return;}//记录主串下标int i = 0;//主串长度int strLen = str.length();//记录子串下标int j = 0;//子串长度int subLen = sub.length();while (i < strLen && j < subLen){if (str.charAt(i) == sub.charAt(j)){i++;j++;}else {//如果不相同了,那么 i 就要回头再来找,而对于 j 就要重头开始了i = i - j + 1;j = 0;}}if (subLen <= j){System.out.println("找到子串再主串的位置了:" + (i-j) + " 到 " + (i-1));}else {System.out.println("没找到!!!!");}}
}

        2.0 KMP 算法概述

        是一种高效的字符串匹配算法,用于在一个主串中查找一个模式串的出现位置。KMP 算法的核心思想是利用已匹配的信息来尽量减少不必要的比较,从而提高匹配效率。

        KMP 算法的时间复杂度为 O(m+n),其中 m 是主串的长度,n 是模式串的长度。相比于 BF 暴力匹配算法,KMP 算法具有更高的效率,尤其在处理大规模文本匹配时表现优异。

        简单来说,KMP 算法比 BF 算法有更高的效率,是 BF 一个升级的算法。

        2.1 KMP 算法实际使用

        同样继续用到 BF 算法的例子。

        举个例子:用 BF 算法来找到主串 str 中是否存在子串 sub,如果存在,那么子串在主串的具体那个位置。

        用变量 i 来记录主串 str 索引下标,用变量 j 来记录子串 sub 索引下标,且用 strLen 来记录主串的长度,用 sunLen 来记录子串的长度。

        2.2 相比于 BF 算法实现,KMP 算法的重要思想

        对于 i 来说:i 不后退,i 一直进行的是 i++ ,即使遇到 str.charAt(i) != sub.charAt(j)  ,i 也不会后退。

        对于 j 来说:当字符都相同 str.charAt(i) == sub.charAt(j) 时,那么 j++ ;当字符不相同 str.charAt(i) != sub.charAt(j) 时,那么 j 会回退到指定的位置,不一定是 0 索引位置。(在 BF 算法中 j 当遇到不相同的时候,一定会回退到 0 索引位置处)

        2.3 为什么要这样设计?

        为了在主串与子串匹配的时候,提高效率。

如图:

        如果按照 BF 算法来设计,那么 i 就会回到索引为 1 位置 b 处,而 j 就要回到索引为 0 位置 a 处。

        而对于 KMP 算法设计来说,当两个字符不相同的时候,i 不用后退,j 不一定退回到索引为 0 处,假设 j 退回到索引为 2 位置 c 处。

        先观察两个圈的位置,从当 j 回到索引为 2 位置 c 处,可以发现子串前面的两个字符与主串的对应的两个字符是一样的,这样就避免了 BF 算法的冗余的比较。

        到底原理是为啥呢?

        发现 a != c 了,但是前面部分肯定是相同的,不然都不会来到此处,那么主串 str 就想着尝试去在 sub 其他位置(除了当前红圈位置的 ab )中找到与主串前部分有没有相同的子字符串,当前就找到了(子串蓝圈部分),那么既然前部分 ab 相同,就不需要比较了,当前比较的是蓝色圈后一个字符是否相同。

        当前来看,是不相同的。那么 i 继续保持不动,j 继续跳到指定的位置,那么假设跳到索引为 0 处的位置。

        发现 str.charAt(i) == sub.charAt(j) 时,i++,j++ ,一直到结束为止。

        2.4 next 数组

        刚刚上面提到了当遇到 str.charAt(i) == sub.charAt(j) 时,i 保持不变而 j 会跳到指定的位置。而这个指定的位置就是 j 对应下标的位置 j = next[j] 。

        2.4.1 创建 next 数组原理

        举个例子来演示

初始化为:

        next 数组中,索引为 0 和索引为 1 分别设置为 -1 和 0。

        接着,到字符 c 的索引下标了,先判断字符 c 前面的字符串有无以 a 开头且以 b 结尾的两个不重复的字符串。显然,这里就两个字符 a b ,没有找到相同的以 a 开头,且以 b 结尾的两个相同且可以不完全重叠的字符串。那么字符 c 的 next 对应就为 0 。

        再接着,到子串索引为 3 处的字符 a ,先判断该字符 a 前面的字符串 a b c 有无以 a 开头且以 c 结尾的两个相同且不完全重叠的字符串,很显然是没有的,同样对应该 next 为 0 。

        再接着,到子串索引为 4 处的字符 b ,先判断 b 字符前面的 a b c a 无以 a 开头且以 a 结尾的两个相同且不完全重叠的字符串。这次发现了存在这样的两个字符串,a 与 a ,长度为 1 。那么对应到 next 数组为 1 。

        再接着,到子串索引为 5 处的字符 c ,先判断 c 字符前面的 a b c a b 有无以 a 开头且以 b 结尾的两个不相同且不完全重叠的字符串。可以明显的发现 ab 与 ab 满足,长度为 2 ,那么对应到 next 数组中为 2 。

        这样 next 数组就创建完毕了。

        再来讲讲具体如何使用 next 数组。

接着上一个例子:

         此时 str.charAt(i) != sub.charAt(j) ,那么 i 保持不动,j 就会根据 next 数组来回到指定的地方,此时 j = next[j] 。因为 j 的值为 5,在 next[5] 中所对应的索引为 2 。

        j 回到索引为 2 处,继续比较 sub.charAt(j) 与 str.charAt(i) 是否相同。如果不相同,i 继续保持不动,j 继续根据 next 数组来给 j 赋值指定的索引;如果相同,那么 i++,j++。

        以上这样情况 a != c ,就要 j 重新赋值 j = next[j] ,则 j = 0 。

        j 回到索引 0 之后,继续比较 sub.charAt(j) 与 str.charAt(i) 是否相同。如果相同,i++,j++ ;如果不相同,i 保持不动,j 就要根据 next 数组来找到对应的值 j = next[j] 。

        以上该情况是相同的,那么直接 i++,j++ 即可。

        补充:当 j = 0 时,发现 sub.charAt(0) 与 str.charAt(i) 还是不相同时,j 根据 next 数组来获取值 j = next[j] 则 j = -1 。这种情况需要特殊考虑,当 j == -1 时,不能再继续比较了,因为会出现数组越界问题,那么该情况应该进行 i++,j++ 操作处理。

        2.4.2 创建 next 数组过程

        1)初始化 next 数组:将 next 数组的第一个元素 next[0] 设置为 -1,next[1] 设置为 0。

        2)遍历模式串:从第二个位置开始(即 i=2),依次计算每个位置 i 处的 next 值。

        3)计算 next 值:具体思路:定义 int k = 0, 从 i = 2 开始,判断子串 sub[i - 1] 与 k 是否相同,如果相同,则 next[i] = k,i++,k++;如果不相同,则 k = next[k] ,直到找到 sub[i-1] 与 k 相同为止,或者 k == -1 为止。

举个例子:

        判断 sub.charAt(k) 与 sub.charAt(i-1) 是否相同,a 与 b很显然不相同,那么 k = next[k] 则 k = -1 ,那么 k == -1 的时候,next[i] = k+1,i++,j++ 。

        此时 k = 0,i = 3 。

        判断 sub.charAt(k) 与 sub.charAt(i-1) 是否相同,a 与 c 很显然不相同,那么 k = next[k] 则 k = -1 ,那么 k == -1 的时候,next[i] = k+1,i++,j++ 。

        此时 k = 0,i = 4 。

        判断 sub.charAt(k) 与 sub.charAt(i-1) 是否相同,a 与 a 是相同的,那么 next[i] = k+1,i++,k++ 。

        此时 next[4] = 1,k = 1,i = 5 。

        判断 sub.charAt(k) 与 sub.charAt(i-1) 是否相同,b 与 b 是相同的,那么 next[5] = k+1,k++,i++ 。

        此时 next[5] = 2,k = 2, i = 5 。

        最后 next 数组就创建完毕了。

        2.5 KMP 算法的实现

        1)在循环过程中,判断主串与子串对应的字符是否相同,如果相同,继续往下比较下去,直到子串遍历完成,说明了主串中存在该子串;如果不相同,记录主串下标的索引保持不变,而记录子串下标的索引需要根据 next 数组来找到相对应的值,接着重新比较子串与主串中字符是否相同,如果相同,继续往下比较;如果不相同,记录子串下标的索引就要继续根据 next 数组来找到指定的位置。

        需要注意的是,当子串下标的索引为 -1 的时候,不能继续往下比较了,该情况为特殊情况,需要进行的操作为:主串往后移动一次,子串的索引 + 1 处理。该特殊情况的操作,跟主串下标对应的字符与子串下标对应的字符相同的情况的操作处理是一致的。

        2)next 数组的创建,首先初始化 next 数组,next[0] = -1,next[1] = 0 。定义 int k = 0,i = 2 ,判断 sub.charAt(i-1) 与 sub.charAt(k) 是否相同,如果相同,next[i] = k+1,i++,k++ ;如果不相同,k = next[k] 。

        需要注意的是,当出现 k == -1 特殊情况的时候,该处理方式为 next[i] = k+1,i++,k++ ,跟 sub.charAt(i-1) 与 sub.charAt(k) 相同处理操作的方式是一致的。

代码如下:

public class demo1 {public static void main(String[] args) {String str = "abcccffggaaffggggkkkllrrr";String sub = "aaffk";kmp(str,sub);}public static void kmp(String str,String sub){if (str == null || sub == null){System.out.println("str 或者 sub 不合法");return;}if (str.length() == 0 || sub.length() == 0){System.out.println(str + " 或者 " + sub + " 长度为 0" );}//用来记录主串的下标int i = 0;//记录主串的长度int strLen = str.length();//用来记录子串的下标int j = 0;//记录子串的长度int subLen = sub.length();//next 数组,存放的是子串与主串不适配所需要 j 回溯的索引下标,长度为字串的长度int[] next = new int[subLen];getNext(next,sub);while (i < strLen && j < subLen){if ( j == -1 || str.charAt(i) == sub.charAt(j)){i++;j++;}else {//当不相同的时候,j 需要回溯到指定的地方j = next[j];}}//判断退出循环的原因if (j >= subLen){System.out.println("找到该主串中子串的位置了:" + (i-j) + " 到 " + (i-1));}else {System.out.println("没有找到!!!");}}public static void getNext(int[] next,String sub){next[0] = -1;next[1] = 0;int i = 2;int k = 0;int len = sub.length();while (i < len){if (k == -1 || sub.charAt(i-1) == sub.charAt(k)){next[i] = k+1;i++;k++;}else {//如果不相同,那么会继续接着找,直到相同为止或者k==-1为止k = next[k];}}}
}

这篇关于Java 算法篇-深入了解 BF 与 KMP 算法的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/918330

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06