字符串匹配的KMP算法和C语言代码,不需要思考就能理解</h1>

2024-05-28 16:38

本文主要是介绍字符串匹配的KMP算法和C语言代码,不需要思考就能理解</h1>,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

KMP算法用于判断一个字符串是否包含另一个字符串,如果包含就返回脚标。其实KMP算法本身特别简单,我看了几篇本章都号称简单易懂,结果看得我云里雾里,直到我看到了阮一峰:字符串匹配的KMP算法,才真正看懂。下文的第一部分基本上都是阮一峰文章的转述,第二部分代码也是在网上其他地方找的。第三部分KMP算法的理解,才是笔者的东西,相信看了第三部分你会豁然开朗。

一、KMS算法的处理过程

下面用 KMP算法在字符串"BBC ABCDAB ABCDABCDABDE"中寻找字符串"ABCDABD":

首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

因为B与A不匹配,搜索词再往后移。

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

接着比较字符串和搜索词的下一个字符,还是相同。

直到字符串有一个字符,与搜索词对应的字符不相同为止。

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP 算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

    已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的。查表可知,最后一个匹配字符B对应的"部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

    移动位数 = 已匹配的字符数 - 对应的部分匹配值

    因为 6 - 2 等于4,所以将搜索词向后移动 4 位。

移动完后,再比较,发现空格与C还是不匹配,于是搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移 2 位。

这时空格与A还是不匹配,继续后移一位。(因为搜索词已经移动到头了,所以默认的移动距离为1位)

这个时候发现能匹配了,于是又逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动 4 位。

这下就完全匹配了。

下面介绍《部分匹配表》是如何产生的。

  首先,要了解两个概念:"前缀"和"后缀"。 "前缀"指除了最后一个字符以外,一个字符串的全部头部组合;"后缀"指除了第一个字符以外,一个字符串的全部尾部组合。

        比如“bread”这个字符串的前缀是除了最后一个字符“d”,还剩下"brea",它的全部组合字符串有:"b"、"br"、"bre"、"brea"。

        “bread”这个字符串的后缀是除了第一个字符“b”,还剩下"read",它的全部组合字符串有:"r"、"re"、"rea"、"read"。

        "部分匹配值"就是"前缀"和"后缀"的最长的共有元素的长度。以"ABCDABD"为例,

-"A"的前缀和后缀都为空集,共有元素的长度为0;

-"AB"的前缀为[A],后缀为[B],共有元素的长度为0;

-"ABC"的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

-"ABCD"的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

-"ABCDA"的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为"A",长度为1;

-"ABCDAB"的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2;

-"ABCDABD"的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

         "部分匹配值"其实还可以定义为:把字符串沿着中间的字符折叠,字符串首尾重叠的字符个数。

        比如“ABC”沿着字符“B”折叠,首尾的“A”和“C”就不能重叠,所以"部分匹配值"为0.

        "ABCDA"沿着字符“C”折叠,首尾的“A”能重叠,所以"部分匹配值"为1.

        "ABCDAB"沿着字符“C”和“D”的中间折叠,首尾的“AB”能重叠,所以"部分匹配值"为2.

二、C语言程序

        利用上面KMS的处理过程和部分匹配值的生成方法,就可以写程序了:

  1. void cal_next(char *str, int *next, int len)
  2. {
  3. next[0] = -1;//next[0]初始化为-1,-1表示不存在相同的最大前缀和最大后缀
  4. int k = -1;//k初始化为-1
  5. for (int q = 1; q <= len-1; q++)
  6. {
  7. while (k > -1 && str[k + 1] != str[q])//如果下一个不同,那么k就变成next[k],注意next[k]是小于k的,无论k取任何值。
  8. {
  9. k = next[k];//往前回溯
  10. }
  11. if (str[k + 1] == str[q])//如果相同,k++
  12. {
  13. k = k + 1;
  14. }
  15. next[q] = k;//这个是把算的k的值(就是相同的最大前缀和最大后缀长)赋给next[q]
  16. }
  17. }
  18. int KMP(char *str, int slen, char *ptr, int plen)
  19. {
  20. int *next = new int[plen];
  21. cal_next(ptr, next, plen);//计算next数组
  22. int k = -1;
  23. for (int i = 0; i < slen; i++)
  24. {
  25. while (k >-1&& ptr[k + 1] != str[i])//ptr和str不匹配,且k>-1(表示ptr和str有部分匹配)
  26. k = next[k];//往前回溯
  27. if (ptr[k + 1] == str[i])
  28. k = k + 1;
  29. if (k == plen-1)//说明k移动到ptr的最末端
  30. {
  31. //cout << "在位置" << i-plen+1<< endl;
  32. //k = -1;//重新初始化,寻找下一个
  33. //i = i - plen + 1;//i定位到该位置,外层for循环i++可以继续找下一个(这里默认存在两个匹配字符串可以部分重叠),感谢评论中同学指出错误。
  34. return i-plen+1;//返回相应的位置
  35. }
  36. }
  37. return -1;
  38. }

测试程序:

  1. char *str = "bacbababadababacambabacaddababacasdsd";
  2. char *ptr = "ababaca";
  3. int a = KMP(str, 36, ptr, 7);
  4. return 0;

三、KMS的理解

当搜索词已经匹配了6个字符,最后一个字符不匹配时

传统的做法是移动一位,然后重新比较

可是KMS却是直接移动了6 - 2 =4位:

这是为什么呢?

怎么保证的移动1位、移动2位、移动3位时遇到的字符串都不匹配,所以KMS能直接移动4位,再去比较呢?

其实非常简单,这就是字符串沿中间折叠后,首尾字符重叠个数(也就是部分匹配值)的作用所在。

下面我举个例子就懂了:

比如要在字符串“123456789”中找“4568”

当匹配到上图的位置时,发现字符“7”和字符“8”不匹配,于是传统的方法是向后移动一位:

而KMS的移动距离用公式计算为:3-0=3位

KMS算法把“4568”中已经匹配过的“456”,直接跳过去了。为什么这里能直接把匹配过的字符串“456“跳过去呢?

下面我们来讨论,我们把已经匹配过的字符分5种情况:(以3个字符来举例)

1)、已经匹配过的字符串中,每个字符都不相同,如”456“

    如果是这种情况,毫无疑问是可以直接把已匹配的字符跳过去的。

2)、已经匹配过的字符串中,每个字符都相同,如”444“

    这种情况,最多只能移动1位,比如下面的例子:

3)、已经匹配过的字符串中,首尾字符相同,如”454“

    这种情况,移动位数要大于1,小于等于2。比如:

移动一位后:

首字符会和中间的字符对齐,他们不相同。再移动一位,就可能完全匹配了,因此移动位数最多为2。

4)、已经匹配过的字符串中,首两个字符相同,如”445“

移动一位:

最后两个字符会和首两个字符对齐,他们一定不相同。

移动两位:

最后一个字符会和首字符对齐,他们一定不相同。因此移动位数最少为3。

5)、已经匹配过的字符串中,尾两个字符相同,如”455“

同理,移动位数最少为3。

        

        经过上面的例子分析,可以看出KMS算法就是对已经匹配过的字符串再次利用来提高效率的。

        KMS算法可以一句话总结为:已经匹配过的字符串如果首尾有相同的字母,就要少移动相应的位数,否则,已经匹配了多少个字符,就可以移动多少位。

这篇关于字符串匹配的KMP算法和C语言代码,不需要思考就能理解</h1>的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

JAVA中整型数组、字符串数组、整型数和字符串 的创建与转换的方法

《JAVA中整型数组、字符串数组、整型数和字符串的创建与转换的方法》本文介绍了Java中字符串、字符数组和整型数组的创建方法,以及它们之间的转换方法,还详细讲解了字符串中的一些常用方法,如index... 目录一、字符串、字符数组和整型数组的创建1、字符串的创建方法1.1 通过引用字符数组来创建字符串1.2

深入理解C++ 空类大小

《深入理解C++空类大小》本文主要介绍了C++空类大小,规定空类大小为1字节,主要是为了保证对象的唯一性和可区分性,满足数组元素地址连续的要求,下面就来了解一下... 目录1. 保证对象的唯一性和可区分性2. 满足数组元素地址连续的要求3. 与C++的对象模型和内存管理机制相适配查看类对象内存在C++中,规

SpringCloud集成AlloyDB的示例代码

《SpringCloud集成AlloyDB的示例代码》AlloyDB是GoogleCloud提供的一种高度可扩展、强性能的关系型数据库服务,它兼容PostgreSQL,并提供了更快的查询性能... 目录1.AlloyDBjavascript是什么?AlloyDB 的工作原理2.搭建测试环境3.代码工程1.

Java调用Python代码的几种方法小结

《Java调用Python代码的几种方法小结》Python语言有丰富的系统管理、数据处理、统计类软件包,因此从java应用中调用Python代码的需求很常见、实用,本文介绍几种方法从java调用Pyt... 目录引言Java core使用ProcessBuilder使用Java脚本引擎总结引言python

Java中ArrayList的8种浅拷贝方式示例代码

《Java中ArrayList的8种浅拷贝方式示例代码》:本文主要介绍Java中ArrayList的8种浅拷贝方式的相关资料,讲解了Java中ArrayList的浅拷贝概念,并详细分享了八种实现浅... 目录引言什么是浅拷贝?ArrayList 浅拷贝的重要性方法一:使用构造函数方法二:使用 addAll(

JAVA利用顺序表实现“杨辉三角”的思路及代码示例

《JAVA利用顺序表实现“杨辉三角”的思路及代码示例》杨辉三角形是中国古代数学的杰出研究成果之一,是我国北宋数学家贾宪于1050年首先发现并使用的,:本文主要介绍JAVA利用顺序表实现杨辉三角的思... 目录一:“杨辉三角”题目链接二:题解代码:三:题解思路:总结一:“杨辉三角”题目链接题目链接:点击这里

SpringBoot使用注解集成Redis缓存的示例代码

《SpringBoot使用注解集成Redis缓存的示例代码》:本文主要介绍在SpringBoot中使用注解集成Redis缓存的步骤,包括添加依赖、创建相关配置类、需要缓存数据的类(Tes... 目录一、创建 Caching 配置类二、创建需要缓存数据的类三、测试方法Spring Boot 熟悉后,集成一个外

C语言线程池的常见实现方式详解

《C语言线程池的常见实现方式详解》本文介绍了如何使用C语言实现一个基本的线程池,线程池的实现包括工作线程、任务队列、任务调度、线程池的初始化、任务添加、销毁等步骤,感兴趣的朋友跟随小编一起看看吧... 目录1. 线程池的基本结构2. 线程池的实现步骤3. 线程池的核心数据结构4. 线程池的详细实现4.1 初

C#中字符串分割的多种方式

《C#中字符串分割的多种方式》在C#编程语言中,字符串处理是日常开发中不可或缺的一部分,字符串分割是处理文本数据时常用的操作,它允许我们将一个长字符串分解成多个子字符串,本文给大家介绍了C#中字符串分... 目录1. 使用 string.Split2. 使用正则表达式 (Regex.Split)3. 使用

轻松掌握python的dataclass让你的代码更简洁优雅

《轻松掌握python的dataclass让你的代码更简洁优雅》本文总结了几个我在使用Python的dataclass时常用的技巧,dataclass装饰器可以帮助我们简化数据类的定义过程,包括设置默... 目录1. 传统的类定义方式2. dataclass装饰器定义类2.1. 默认值2.2. 隐藏敏感信息