文章目录

  • 一、暴力穷解法
  • 二、KMP算法
  • 二、BM算法
  • 三、Sunday算法
  • 四、完整代码

所有的LeetCode题解索引,可以看这篇文章——【算法和数据结构】LeetCode题解。

一、暴力穷解法

  思路分析:首先判断字符串是否合法,然后利用for循环,取出子字符串利用compare函数进行比较。
  程序如下:

class Solution {public:// 复杂度n * mint strStr(string haystack, string needle) {if (haystack.size() < needle.size()) return -1;    if (!needle.size()) return 0; // needle为空返回0for (int i = 0; i < haystack.size(); ++i) {string substr = haystack.substr(i, needle.size());if (!needle.compare(substr)) return i;}return -1;}
};

复杂度分析:

  • 时间复杂度: O ( n ∗ m ) O(n * m) O(n∗m),假设haystack的长度为n,needle的长度为m,for循环的复杂度为n,当中调用了compare函数,它是逐字符比较的,复杂度为m,因此总复杂度为 O ( n ∗ m ) O(n * m) O(n∗m)。
  • 空间复杂度: O ( 1 ) O(1) O(1)。

二、KMP算法

  KMP成功实现了字符串匹配算法从乘法复杂度到线性复杂度的跨越。KMP算法是由这三位学者发明的:Knuth,Morris和Pratt,所以得名KMP算法。KMP算法的主要思想是:当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配。在KMP算法当中,利用next数组记录前缀表(prefix table)。前缀表用来回退,它记录了模式串与文本串不匹配的时候,模式串应该从哪里开始重新匹配。文本串是指要搜寻的字符串,模式串是目标字符串。例如,要在aabaaf当中找aaf,那么aaf就是模式串,aabaaf就是文本串。
  在next数组当中我们保存了模式串前缀的最长公共前后缀长度,以下简称为前缀表。字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串,所谓最长公共前后缀,笔者理解为既在前缀中,又在后缀中,且长度最大的字符串。例如对于字符串 aabaa,其前缀有 a, aa, aab, aaba,后缀有a, aa, baa, abaa。最长公共前后缀就是 aa。
  那么为什么前缀表能告诉我们上次匹配的位置呢?考虑一个文本串aabaabaafa和模式串aabaaf。求出前缀表如下:

  假设我们在f位置,文本字符和模式字符不匹配(aabaa与模式串全部匹配),那么我们就得找f字符的前一个前缀字符串,也就是aabaa,前缀表next当中保存着所有前缀最长公共前后缀长度,aabaa的最长公共前后缀长度为2。也就是说,后缀aa匹配,那么前缀aa也必然和模式串匹配,因此前缀aa不需要再进行对比,直接让模式串的指针按next表跳转到下一个位置,因为next数组当中存储的数值为2,所以模式串跳转到下标为2的地方进行对比。(图片引用自代码随想录28.strStr)。

  其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
复杂度分析:

  • 时间复杂度: O ( n + m ) O(n + m) O(n+m),其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。
  • 空间复杂度: O ( m ) O(m) O(m),需要额外的空间存放大小为m的数组。

  KMP的关键在于构建next数组,getNext函数用来计算最长公共前后缀。本文使用的是前缀表统一减一的方式,令模式串的首字符最长公共前缀为-1。对于KMP算法来说,前缀表无论是统一减一或者是不减一都只是一种实现方式,其核心仍然是KMP算法。程序当中,i代表是模式串子串的最右端字符,当字符不相等时使用while循环回退,字符相等时就令j++,同时将j的值记录在前缀表当中。
KMP算法程序:

 // KMP算法void getNext(int* next, const string& s) {int j = -1;next[0] = j;for (int i = 1; i < s.size(); i++) { // 注意i从1开始while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了或者没有最长公共前后缀j = next[j]; // 向前回退}if (s[i] == s[j + 1]) { // 找到相同的前后缀j++;}next[i] = j; // 将j(前缀的长度)赋给next[i]}}int strStr2(string haystack, string needle) {if (!needle.size()) return 0;int* next = new int[needle.size()];getNext(next, needle);//my_print(next, needle.size(), "前缀表:");int j = -1; // // 因为next数组里记录的起始位置为-1for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始while (j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配j = next[j]; // j 寻找之前匹配的位置}if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动j++; // i的增加在for循环里}if (j == (needle.size() - 1)) { // 文本串s里出现了模式串treturn (i - needle.size() + 1);}}return -1;}

二、BM算法

  本节转载自从头到尾彻底理解KMP。
  1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法,简称BM算法。该算法从模式串的尾部开始匹配,且拥有在最坏情况下O(N)的时间复杂度。在实践中,比KMP算法的实际效能高。BM算法定义了两个规则:

  • 坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果"坏字符"不包含在模式串之中,则最右出现位置为-1。
  • 好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。
      下面举例说明BM算法。例如,给定文本串“HERE IS A SIMPLE EXAMPLE”,和模式串“EXAMPLE”,现要查找模式串是否在文本串中,如果存在,返回模式串在文本串中的位置。

    1. 首先,"文本串"与"模式串"头部对齐,从尾部开始比较。"S"与"E"不匹配。这时,“S"就被称为"坏字符”(bad character),即不匹配的字符,它对应着模式串的第6位。且"S"不包含在模式串"EXAMPLE"之中(相当于最右出现位置是-1),这意味着可以把模式串后移6-(-1)=7位,从而直接移到"S"的后一位。
    2. 依然从尾部开始比较,发现"P"与"E"不匹配,所以"P"是"坏字符"。但是,"P"包含在模式串"EXAMPLE"之中。因为“P”这个“坏字符”对应着模式串的第6位(从0开始编号),且在模式串中的最右出现位置为4,所以,将模式串后移6-4=2位,两个"P"对齐。

    3. 依次比较,得到 “MPLE”匹配,称为"好后缀"(good suffix),即所有尾部匹配的字符串。注意,“MPLE”、“PLE”、“LE”、"E"都是好后缀。
    4. 发现“I”与“A”不匹配:“I”是坏字符。如果是根据坏字符规则,此时模式串应该后移2-(-1)=3位。问题是,有没有更优的移法?

    5. 更优的移法是利用好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串中上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。所有的“好后缀”(MPLE、PLE、LE、E)之中,只有“E”在“EXAMPLE”的头部出现,所以后移6-0=6位。可以看出,“坏字符规则”只能移3位,“好后缀规则”可以移6位。每次后移这两个规则之中的较大值。这两个规则的移动位数,只与模式串有关,与原文本串无关。
    6. 继续从尾部开始比较,“P”与“E”不匹配,因此“P”是“坏字符”,根据“坏字符规则”,后移 6 - 4 = 2位。因为是最后一位就失配,尚未获得好后缀。

        由上可知,BM算法不仅效率高,而且构思巧妙,容易理解。

三、Sunday算法

  KMP算法并不比最简单的c库函数strstr()快多少,而BM算法虽然通常比KMP算法快,但BM算法也还不是现有字符串查找算法中最快的算法,本文最后再介绍一种比BM算法更快的查找算法即Sunday算法。Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很相似:只不过Sunday算法是从前往后匹配,在匹配失败时关注的是文本串中参加匹配的最末位字符的下一位字符。

  • 如果该字符没有在模式串中出现则直接跳过,即移动位数 = 匹配串长度 + 1;
  • 否则,其移动位数 = 模式串中最右端的该字符到末尾的距离+1。
    // Sunday算法int find_single_char(char c, const string& needle) {   for (int i = needle.size() - 1; i >= 0; --i) {  // 找最右端的字符,因此从后往前循环if (c == needle[i]) return i; }return -1;}int strStr3(string haystack, string needle) {if (haystack.size() < needle.size()) return -1;     // 检查合法性if (!needle.size()) return 0;                       // needle为空返回0      for (int i = 0; i <= haystack.size() - needle.size(); ) {for (int j = 0; j < needle.size(); ++j) {if (needle[j] != haystack[i + j]) {     // 匹配失败                int k = find_single_char(haystack[i + needle.size()], needle);   // 文本字符串末尾的下一位字符串if (k == -1)   i += needle.size() + 1;  // 模式串向右移动 模式串长度 + 1 else i += needle.size() - k;            // 向右移动 模式串最右端的该字符到末尾的距离+1break;}     if (j == needle.size() - 1) return i;   // 匹配成功}}return -1;}

  除了上面一个版本的代码之外,笔者还写了另外一个版本的代码。可以观察到,我们通过find_single_char这个函数去查找字符串是否存在这个字符的效率是比较低的。想要快速的查找一个元素是否在一个字符串当中,不得不考虑用哈希表。字符串在编译器内存是使用ASCII码,因此我们建立一个ASCII码表数组,提前计算出模式串的偏移量,代码如下:

    // 查找算法用哈希表代替的Sunday算法   int strStr4(string haystack, string needle) {if (haystack.size() < needle.size()) return -1;     // 检查合法性if (!needle.size()) return 0;                       // needle为空返回0  int shift_table[128] = { 0 };       // 128为ASCII码表长度for (int i = 0; i < 128; i++) {     // 偏移表默认值设置为 模式串长度 + 1shift_table[i] = needle.size() + 1;}for (int i = 0; i < needle.size(); i++) {shift_table[needle[i]] = needle.size() - i;}int s = 0, j; // 文本串初始位置while (s <= haystack.size() - needle.size()) {j = 0;while (haystack[s + j] == needle[j]) { ++j;if (j >= needle.size()) return s;   // 匹配成功}// 找到主串中当前跟模式串匹配的最末字符的下一个字符// 在模式串中出现最后的位置// 所需要从(模式串末尾+1)移动到该位置的步数s += shift_table[haystack[s + needle.size()]];}return -1;}

复杂度分析:

  • 时间复杂度: 平均时间复杂度为 O ( n ) O(n) O(n),最坏情况时间复杂度为 O ( n ∗ m ) O(n*m) O(n∗m)。
  • 空间复杂度: O ( 1 ) O(1) O(1),常量存储空间。

四、完整代码

  代码当中包含了暴力穷解法、KMP算法、Sunday算法。

# include <iostream>
# include <string>
using namespace std;void my_print(int* arr, int arr_len, string str) {cout << str << endl;for (int i = 0; i < arr_len; ++i) {cout << arr[i] << ' ';}cout << endl;
}class Solution {public:// 暴力穷解// 复杂度n * mint strStr(string haystack, string needle) {if (haystack.size() < needle.size()) return -1; if (!needle.size()) return 0; // needle为空返回0for (int i = 0; i < haystack.size(); ++i) {string substr = haystack.substr(i, needle.size());if (!needle.compare(substr)) return i;}return -1;}// KMP算法void getNext(int* next, const string& s) {int j = -1;next[0] = j;for (int i = 1; i < s.size(); i++) { // 注意i从1开始while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了j = next[j]; // 向前回退}if (s[i] == s[j + 1]) { // 找到相同的前后缀j++;}next[i] = j; // 将j(前缀的长度)赋给next[i]}}int strStr2(string haystack, string needle) {if (needle.size() == 0) {return 0;}int* next = new int[needle.size()];//int next[needle.size()]; getNext(next, needle);//my_print(next, needle.size(), "前缀表:");int j = -1; // // 因为next数组里记录的起始位置为-1for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始while (j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配j = next[j]; // j 寻找之前匹配的位置}if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动j++; // i的增加在for循环里}if (j == (needle.size() - 1)) { // 文本串s里出现了模式串treturn (i - needle.size() + 1);}}return -1;}// Sunday算法int find_single_char(char c, const string& needle) {   for (int i = needle.size() - 1; i >= 0; --i) {  // 找最右端的字符,因此从后往前循环if (c == needle[i]) return i; }return -1;}int strStr3(string haystack, string needle) {if (haystack.size() < needle.size()) return -1;     // 检查合法性if (!needle.size()) return 0;                       // needle为空返回0      for (int i = 0; i <= haystack.size() - needle.size(); ) {for (int j = 0; j < needle.size(); ++j) {if (needle[j] != haystack[i + j]) {     // 匹配失败                int k = find_single_char(haystack[i + needle.size()], needle);   // 文本字符串末尾的下一位字符串if (k == -1)   i += needle.size() + 1;  // 模式串向右移动 模式串长度 + 1 else i += needle.size() - k;            // 向右移动 模式串最右端的该字符到末尾的距离+1break;}     if (j == needle.size() - 1) return i;   // 匹配成功}}return -1;}// 查找算法用哈希表代替的Sunday算法   int strStr4(string haystack, string needle) {if (haystack.size() < needle.size()) return -1;     // 检查合法性if (!needle.size()) return 0;                       // needle为空返回0  int shift_table[128] = { 0 };       // 128为ASCII码表长度for (int i = 0; i < 128; i++) {     // 偏移表默认值设置为 模式串长度 + 1shift_table[i] = needle.size() + 1;}for (int i = 0; i < needle.size(); i++) {shift_table[needle[i]] = needle.size() - i;}int s = 0, j; // 文本串初始位置while (s <= haystack.size() - needle.size()) {j = 0;while (haystack[s + j] == needle[j]) { ++j;if (j >= needle.size()) return s;   // 匹配成功}// 找到主串中当前跟模式串匹配的最末字符的下一个字符// 在模式串中出现最后的位置// 所需要从(模式串末尾+1)移动到该位置的步数s += shift_table[haystack[s + needle.size()]];}return -1;}
};int main()
{//string haystack = "sadbutsad";//string needle = "sad";//string haystack = "abc";//string needle = "c";//string haystack = "substring searching algorithm";//string needle = "search";//string haystack = "hello";//string needle = "ll";//string haystack = "mississippi";//string needle = "issi";string haystack = "aabaaaababaababaa";string needle = "bbbb";int k = 2;Solution s1;cout << "目标字符串:\n" << "“" << haystack << "”" << endl;int result = s1.strStr4(haystack, needle);cout << "查找子串结果:\n" << result << endl;system("pause");return 0;
}

参考博文:

  • 最长公共前后缀
  • 字符串最长公共前缀后缀长度
  • 从头到尾彻底理解KMP
  • 字符串匹配——Sunday算法

【算法与数据结构】字符串匹配算法相关推荐

  1. diff算法阮一峰_【重学数据结构与算法(JS)】字符串匹配算法(三)——BM算法

    前言 文章的一开头,还是要强调下字符串匹配的思路 将模式串和主串进行比较 从前往后比较 从后往前比较 2. 匹配时,比较主串和模式串的下一个位置 3. 失配时, 在模式串中寻找一个合适的位置 如果找到 ...

  2. 大量的数据做字符串匹配_【重学数据结构与算法(JS)】字符串匹配算法(三)——BM算法...

    前言 文章的一开头,还是要强调下字符串匹配的思路 将模式串和主串进行比较 从前往后比较 从后往前比较 2. 匹配时,比较主串和模式串的下一个位置 3. 失配时, 在模式串中寻找一个合适的位置 如果找到 ...

  3. 0x00000005 3.数据结构和算法 基础数据结构 字符串(上)

    文章目录 基本知识简单总结 模式匹配 最长回文子串 前缀匹配 扩展和补充* C++11常见API References: 字符串也是一个高频考察点. 虽然可以和数组考点合并,但由于该场景许多优化空间大 ...

  4. 每周一算法之六——KMP字符串匹配算法

    KMP是一种著名的字符串模式匹配算法,它的名称来自三个发明人的名字.这个算法的一个特点就是,在匹配时,主串的指针不用回溯,整个匹配过程中,只需要对主串扫描一遍就可以了.因此适合对大字符串进行匹配. 搜 ...

  5. 算法之「字符串匹配算法」

    前言 一说到两个字符串匹配,我们很自然就会想到用两层循环来匹配,用这种方式就可以实现一个字符串是否包含另一个字符串了,这种算法我们称为 BF算法. BF算法 BF算法,即暴力(Brute Force) ...

  6. 字符串匹配算法之暴力做法(朴素算法)

    字符串匹配算法之暴力做法(朴素算法) 1.字符串匹配算法 1.1 简介 1.2 类型 1.3 示例题目 2.暴力做法(朴素算法) 2.1 暴力算法的思路 2.2 暴力算法的特点: 2.3 暴力算法的J ...

  7. ZZL字符串匹配算法

    分类: 算法艺术2009-12-31 13:25 2579人阅读 评论(4) 收藏 举报 算法standards存储数据结构搜索引擎语言 转载一篇关于字符串匹配算法ZZL的论文, 图片有点问题,将就着 ...

  8. 4种字符串匹配算法:有限自动机(中)

    接着上文(地址),我们来聊一聊自动机算法(有限自动机字符串匹配算法)和KMP算法. ====#=有限自动机算法=#===== 关于有限自动机,网上的分析的资源,大部分都很笼统,算导上的知识点,全是数学 ...

  9. iptables --algo 字符串匹配算法 bm kmp

    http://blog.csdn.net/l953972252/article/details/51331001 字符串匹配一直是计算机领域热门的研究问题之一,多种算法层出不穷.字符串匹配算法有着很强 ...

  10. 这可能是全网最好的字符串匹配算法讲解

    点击上方 好好学java ,选择 星标 公众号重磅资讯,干货,第一时间送达 今日推荐:14 个 github 项目!个人原创100W +访问量博客:点击前往,查看更多 为保证代码严谨性,文中所有代码均 ...

最新文章

  1. SAP 如何将无序列号的库存与序列号关联起来?
  2. 当Elasticsearch遇见Kafka
  3. php 启动服务器监听
  4. require(os)
  5. STL sort()函数详解
  6. 博客园添加一个分享的
  7. VisualC++2010系列课程
  8. C语言for循环的嵌套例题,c语言 for循环的嵌套(含答案)
  9. MindSpore:基于本地差分隐私的 Bandit 算法
  10. Linux C函数之文件及目录函数
  11. 蓝桥杯 ADV-132 算法提高 笨小猴
  12. 计算机三级 信息安全技术题库——选择题1
  13. SpringMVC 配置定时执行任务
  14. pixhawk RC信号传输流程 代码版本pixhawk1.5.5
  15. Power BI 可视化:直观了解分类百分比的饼图树视觉
  16. 嵌入式硬件开发基础(持续更新)
  17. 树-树的遍历(先序、中序、后序)
  18. android电视 优酷视频,将优酷视频投屏到智能电视上,竟然还有这种操作
  19. php 设置raw格式文件,u盘raw格式怎么改过来
  20. Glide硬盘缓存逻辑

热门文章

  1. Qt之实现录音播放及raw(pcm)转wav格式
  2. 七牛云及 HTTP标准状态码总结
  3. jinja2.exceptions.TemplateSyntaxError
  4. miui9如何不自动杀进程_官方没有告诉你的MIUI9十大隐藏使用技巧,助你快速成为小米达人...
  5. 爱福特机器人地址_在福特工厂,波士顿动力公司的机器狗现场与一个新的机器人朋友组队
  6. Vczh Library++3.0实现二进制模板函数
  7. deepin linux--安装virtualbox
  8. i7 12700参数 i712700怎么样
  9. json格式数据下载为excel方法,其中数字格式下载后排序正常
  10. 怎样判断DLL是32位还是64位的