C++之单字符串匹配问题

article/2025/10/3 23:58:55

有很多算法可以解决单模式匹配问题。而根据在文本中搜索模式串方式的不同,我们可以将单模式匹配算法分为以下几种:

  • 基于前缀搜索方法:在搜索窗口内从前向后(沿着文本的正向)逐个读入文本字符,搜索窗口中文本和模式串的最长公共前缀。
    • 著名的 Knuth-Morris-Pratt (KMP) 算法和更快的 Shift-Or 算法使用的就是这种方法。
  • 基于后缀搜索方法:在搜索窗口内从后向前(沿着文本的反向)逐个读入文本字符,搜索窗口中文本和模式串的最长公共后缀。使用这种搜索算法可以跳过一些文本字符,从而具有亚线性的平均时间复杂度。
    • 最著名的 Boyer-Moore 算法,以及 Horspool 算法、Sunday (Boyer-Moore 算法的简化) 算法都使用了这种方法。
  • 基于子串搜索方法:在搜索窗口内从后向前(沿着文本的反向)逐个读入文本字符,搜索满足「既是窗口中文本的后缀,也是模式串的子串」的最长字符串。与后缀搜索方法一样,使用这种搜索方法也具有亚线性的平均时间复杂度。这种方法的主要缺点在于需要识别模式串的所有子串,这是一个非常复杂的问题。
    • Rabin-Karp 算法、Backward Dawg Matching (BDM) 算法、Backward Nondeterministtic Dawg Matching (BNDM) 算法和 Backward Oracle Matching (BOM) 算法使用的就是这种思想。其中,Rabin-Karp 算法使用了基于散列的子串搜索算法。

摘取自01.字符串基础知识 | 算法通关手册 (itcharge.cn)

这里主要是通过C++实现单模式串匹配问题

  • Brute Force算法
  • Rabin Karp算法
  • KMP算法
  • Boyer Moore算法
  • Horspool算法
  • Sunday算法

Brute Force算法

中文意思是暴力匹配算法,也可以叫做朴素匹配算法。

算法思想:将子串的第一个元素与主串的每一个元素逐一对比,直到在主串中匹配到该子串,或者找不到匹配子串。

代码实现如下:

int BruteForce(const string& t, const string& p) {int i,j;i = j = 0;while (i < t.size() && j < p.size()) {if (t[i] == p[j]) {i += 1;j += 1;}else {//这波恢复属实精彩i = i - (j - 1);j = 0;}}if (j == p.size()) return i - j;else return -1;
}

代码较为简单,要注意的是,当匹配到n个元素,出现错误时,需要回到最开始匹配的地方。

时间复杂度分析:(n为主串长度,m为子串长度)

  • 最好的情况:直接匹配成功,O(m)
  • 最换的情况:每一次匹配都要进行m次匹配,总共要进行n-m+1
  • 平均时间复杂度:在一般的情况下,根据等概率原则,时间复杂度O(n+m)

Rabin Karp算法

Rabin Karp算法简称为RK算法,

算法思想:对于给定文本串 T 与模式串 p,通过滚动哈希算法快速筛选出与模式串 p 不匹配的文本位置,然后在其余位置继续检查匹配项。

代码实例

int RabinKarp(const string& t, const string& p, int d,int q) {if (t.size() < p.size()) return -1;int hash_p, hash_t; hash_p = hash_t = 0;// 获取p和t的哈希值for (int i = 0; i < p.size(); i++) {hash_p = (hash_p * d + int(p[i]-'a')) % q;hash_t = (hash_t * d + int(t[i]-'a')) % q;}// 最大一位为1的值int power = int(pow(d,int(p.size()) - 1)) % q;// 开始进行匹配for (int i = 0; i < t.size() - p.size() + 1; i++) {if (hash_p == hash_t) {bool match = true;for (int j = 0; j < p.size(); j++) {if (t[i + j] != p[j]) {match = false;break;}}if (match) return i;}if (i < t.size() - p.size()) {hash_t = (hash_t - power * int(t[i]-'a')) % q;   //减去前一个值hash_t = (hash_t * d + int(t[i + p.size()]-'a')) % q;  //加入新值hash_t = (hash_t + q) % q;  //确保 hash_t > 0}}return -1;
}

对于这个RK算法,我们用到了4个参数,主串t,子串p,进制数d,以及最大取余数q。

RK算法主要用到了哈希函数,且采用的是进制hash函数。

比如 cat 的哈希值就可以表示为:
H a s h ( c a t ) = c × 26 × 26 + a × 26 + t × 1 = 2 × 26 × 26 + 0 × 26 + 19 × 1 = 1371 \begin{aligned} Hash(cat)&=c×26×26+a×26+t×1\\ &=2×26×26+0×26+19×1\\ &=1371 \end{aligned} Hash(cat)=c×26×26+a×26+t×1=2×26×26+0×26+19×1=1371

比如你的匹配字符串中有26个字符,那么可以采用26进制,这样就将cat通过哈希函数转换成了整数。不过为了防止数值过大而溢出,需要采用q对于计算的结果取余。

了解哈希函数之后,我们再来看代码。

  1. 防止p子串长度大于t主串。
  2. 计算p子串的哈希值,也计算和p相同长度的t截取部分的哈希值。
  3. 同时先储存p最高位且系数为1的值,方便后续计算。
  4. 然后遍历主串,从0到n-m+1,如果t的哈希值与p的哈希值相同,就进行后续的匹配操作,匹配成功则返回索引,失败了则继续循环。
  5. 如果上述匹配没有成功,那么就需要移动一步,并且重新计算t的哈希值,不过只需要去掉最高位,在重新计算就可以了。

可能有人会有疑惑,为什么哈希值相同了还需要进行匹配操作,那是因为哈希函数存在一个问题,那就是哈希冲突,即不同的子串却得到了相同的哈希值,因此最后还需要进行一步验证。

时间复杂度分析

如果不存在哈希冲突问题,那么时间复杂度为O(n),只需要遍历一次即可。

最坏的情况则是每次都会出现冲突,也就是BF算法一样,时间复杂度为O(m*n)

这里实现的RK算法,只针对小写字母,大写字符以及大小写混合,还需要修改提取ASCII值的那个部分。

KMP算法

KMP 算法思想:对于给定文本串 T 与模式串 p,当发现文本串 T 的某个字符与模式串 p 不匹配的时候,可以利用匹配失败后的信息,尽量减少模式串与文本串的匹配次数,避免文本串位置的回退,以达到快速匹配的目的。

这里列举一个简单的例子

请添加图片描述

请添加图片描述

我们可以看到,KMP算法相比于BF算法,在匹配失败之后,KMP回退了2格,而BF算法要回退了4格,即起始位置的后一位。显然KMP算法的效率是更快的。那么他是怎么实现的呢?

它应用了额外的空间,即next数组。我们可以这样想,子串长度是3,那么无非就是4种情况,1.无匹配;2.匹配一个;3.匹配两个;4.匹配成功。

除掉最后第四种情况,我们只需要将上述三种情况发生,子串应该回退多少记录就行了。这样每次匹配失败,直接根据next数据进行回退就可以了。

当然next的构造肯定是需要通过代码实现的,而不是人为计算。

代码如下

vector<int> GenerateNext(const string& p) {vector<int> next(p.size(), 0);int left = 0;for (int right = 1; right < p.size(); right++) {while (left > 0 && p[left] != p[right])  left = next[left - 1]; //不同的,且left不为0的,等于left-1为索引的数组值。if (p[left] == p[right]) left += 1;  //遇到相同的,则left+1next[right] = left;  //next赋值}return next;
}

这里举一个例子ABCDEFG,他们都是不同的字符,所以都为0。

0 0 0 0 0 0 0

接下来我们再来看ABCABCF

0 0 0 1 2 3 0

这里next数组中的数字表示下一次匹配,可以跳过字符的个数。

当你匹配到第二个B,发现匹配错误,说明前4个字符匹配成功,然后查找最后一个匹配成功的next数组值,即A,值是1,代表下一次匹配可以跳过1个字符。

所以最后算法实现代码如下:

int Kmp(const string& t, const string& p) {vector<int> next = GenerateNext(p);int j = 0;for (auto i = 0; i < t.size(); i++) {while (j > 0 && t[i] != p[j]) j = next[j - 1];  //j-1表示最后一个匹配成功的字符if (t[i] == p[j]) j++;if (j == p.size()) return i-j+1;}return -1;
}

算法实现非常简单;

  1. 获取next数组
  2. 遍历主串
    • 如果匹配失败,且j>0,则通过最后一次匹配成功的next数组值进行回退。
    • 如果一个元素匹配成功,接着匹配下一个。
    • 全部匹配成功,而返回匹配成功字符串的起始位置。

我们可以发现该算法速度极快,只需要O(n+m)。即next数组和一个for循环。

Boyer Moore算法

一种基于后缀的搜索匹配算法,如果想了解详细的原理,可以参考下面这篇文章,浅显易懂。

浅谈字符串匹配算法 —— BM算法_-红桃K的博客-CSDN博客_bm算法的优缺点

如果阅读了上述博客之后,这里我们在进行字符串匹配的代码实现。

首先是坏字符表

unordered_map<char, int> GenerateBadCharTable(const string& p) {unordered_map<char, int> bc_table;for (auto i = 0; i < p.size(); i++) bc_table[p[i]] = i;return bc_table;
}

这里将出现过的字符进行一对一赋值,我们可以发现最后相同字符对应的值为该字符最后一次出现的位置。坏规则表有什么作用呢?

  • 如果匹配到坏字符,发现坏字符不在字典中,则直接大步移动子串到新一位。

  • 如果匹配到坏字符在字典中,可能出现两种情况,这里讲一下位移为负的情况

    a b c c c

    ​ c e c

    这里我们匹配到c和e是不同的,然后我们查到坏字符表总的c的值为2(取字符最后一次出现)这样我们计算移动位置,1-2=-1,反而要想向后移动,这样是不合理的,所以就需要另一个规则,好后缀。

这里时间复杂度为O(m)

接下来是好后缀

vector<int> GenerageSuffixArray(const string& p) {vector<int> suffix(p.size(), 0);for(int i = p.size() - 2; i >= 0; i--) {int start = i;while (start >= 0 && p[start] == p[p.size() - 1 - i + start]) start--;suffix[i] = i - start;   // 更新下标以i结尾的子串与模式后缀匹配的最大长度}return suffix;
}

这里需要先构建suffix数组,其中 suffix[i] = s 表示为以下标 i 为结尾的子串与模式串后缀匹配的最大长度为 s。即满足 p[i-s...i] == p[m-1-s, m-1] 的最大长度为 s

这个数组里面每一个结尾的子串都是和后缀进行比较,所以称为好后缀。

加下来就是生成好后缀后移位数表gs_list代码如下:

这里好后缀规则后移位数表有三种情况

  1. 模式串中有子串匹配上好后缀。
  2. 模式串中无子串匹配上好后缀,但有最长前缀匹配好后缀的后缀。
  3. 模式串中无子串匹配上好后缀,也找不到前缀匹配。
vector<int> GenerageGoodSuffixList(const string& p) {vector<int> gs_list(p.size(), p.size());  //初始化时,假设全部为情况3vector<int> suffix = GenerageSuffixArray(p);int j = 0;  // j为好后缀前的坏字符位置for (int i = p.size() - 1; i >= 0; i--) {  // 情况2 从最长的前缀开始检索if (suffix[i] == i + 1) {  // 匹配到前缀, p[0...i]==p[m-1-i...m-1]while(j < p.size()-1-i){if (gs_list[j] == p.size()) {gs_list[j] = p.size() - 1 - i;   //更新在j处遇到坏字符可向后移动位数}j++;}}}//情况1 匹配到子串 p[i-s...i] == p[m-1-s, m-1]for (int i = 0; i < p.size() - 1; i++) {gs_list[p.size() - 1 - suffix[i]] = p.size() - 1 - i;}return gs_list;
}

这里全程是取后缀进行比较,不必考虑前缀或者内部子串与内部子串是匹配的。这个位移表核心的作用就是如果在第j位出现错误匹配,那么后面的好后缀是否在子串p后面存在相同的匹配,存在则位移到前方。

根据三种情况,如果好后缀匹配不到,则直接赋为最大值。

如果匹配得到,则位移到匹配的地方。这里通过先分析前缀,在分析内部子串来进行全覆盖分析。

  • 前缀的话,计算j在任意匹配位置上出现换字符的移动位数
  • 内部字串的话,直接更新匹配到字符的位置即可。

BM算法时间复杂度分析

  • BM 算法在预处理阶段的时间复杂度为 O(n+σ),其中 σ 是字符集的大小。
  • BM 算法在搜索阶段最好情况是每次匹配时,模式串 p 中不存在与文本串 T 中第一个匹配的字符。这时的时间复杂度为 O(n/m)。
  • BM 算法在搜索阶段最差情况是文本串 T 中有多个重复的字符,并且模式串 p 中有 m - 1 个相同字符前加一个不同的字符组成。这时的时间复杂度为 O(m∗n)。
  • 当模式串 p 是非周期性的,在最坏情况下,BM 算法最多需要进行 3∗n 次字符比较操作。

Horspool算法

「Horspool 算法」 是一种在字符串中查找子串的算法,它是由 Nigel Horspool 教授于 1980 年出版的,是首个对 Boyer Moore 算法进行简化的算法。

代码实现

unordered_map<char, int> GenerateBadCharTable_2(const string& p) {unordered_map<char, int> bc_table;for (auto i = 0; i < p.size()-1; i++) bc_table[p[i]] = p.size()-1-i;  //更新遇到坏字符可向右移动的距离return bc_table;
}

和BM不一样的是,这个坏字符表的数值和BM是相反的,BM是递增,Horspool是递减。相同之处则是,如果遇到相同字符,以最右侧的字符的值为准。

代码实现

int Horspool(const string& t, const string& p) {int n = t.size();int m = p.size();unordered_map<char, int> bc_table = GenerateBadCharTable_2(p);int i = 0;while (i <= n - m) {int j = m - 1;while (j > -1 && t[i + j] == p[j]) j -= 1;  //进行后缀匹配if (j < 0) return i;//失败则通过坏字符表进行移位i += bc_table.find(t[i + m - 1]) != bc_table.end() ? bc_table[t[i + m - 1]] : m;}return -1;
}

这次的实现则只有坏字符表,因为这里的坏字符采用的是逐渐递减的移位方式,如果匹配失败,则对比后缀最后一位数字,来进行移位操作。

  • Horspool 算法在平均情况下的时间复杂度为 O(n),但是在最坏情况下时间复杂度会退化为 O(n∗m)。

Sunday算法

Sunday 算法思想:对于给定文本串 T 与模式串 p,先对模式串 p 进行预处理。然后在匹配的过程中,当发现文本串 T 的某个字符与模式串 p 不匹配的时候,根据启发策略,能够尽可能的跳过一些无法匹配的情况,将模式串多向后滑动几位。

与Horspool不同的是,如果匹配失败,它关注的是参加匹配的末尾字符的下一位字符。

首先是坏字符表

unordered_map<char, int> GenerateBadCharTable_3(const string& p) {unordered_map<char, int> bc_table;for (auto i = 0; i < p.size(); i++) bc_table[p[i]] = p.size() - i;  //更新遇到坏字符可向右移动的距离return bc_table;
}

对于坏字符表和Horspool算法不一样的是,这里少了一个-1。且覆盖到所有元素

然后是代码实现

int Sunday(const string& t, const string& p) {int n = t.size();int m = p.size();unordered_map<char, int> bc_table = GenerateBadCharTable_3(p);int i = 0;while (i <= n - m) {int j = 0;if (t.substr(i, m) == p) return i;  //匹配完成,返回模式串pif (i + m >= n) return -1;i += bc_table.find(t[i + m]) != bc_table.end() ? bc_table[t[i + m]] : m+1;}return -1;
}

首先进行完成的匹配,如果匹配成功,则返回i。如果偏移量i加上子串m的长度大于n,推出,匹配失败。

然后就是位移准则:匹配失败之后,直接匹配主串中匹配失败末尾的后一位字符,进行移位操作。

算法的时间分析

  • Sunday 算法在平均情况下的时间复杂度为 O(n),但是在最坏情况下时间复杂度会退化为 O(n∗m)。

总结:

BM算法非常的复杂,而且也会有效率不高的情况,但是它常态的效率却是KMP的2~3倍,而KMP的效率可以说是非常快了,所以说BM还是非常经典的算法。

每次对同一个问题,不同的解决办法时,我们总需要思考和总结他们对这个问题的优化思路是什么?

  • 牺牲一定的效率,提高代码的健壮性和稳定性。
  • 面对某个多次调动的表达式,通过预处理,或者缓存来实现加速。
  • 通过空间来换时间。
  • 又或者对于查找,采用hash结构来提高效率。

等等,都是值得我们思考和总结的。


如果有用希望得到大家的二连,同时也感觉大家的阅读。


http://chatgpt.dhexx.cn/article/X4Dlgcw5.shtml

相关文章

字符串——字符串匹配

文章目录 字符串匹配一、基本概念字符串匹配问题符号和术语后缀重叠引理 二、朴素字符串匹配算法三、Rabin-Karp算法(字符串Hash算法)进制Hash质数Hash 四、利用有限自动机进行字符串匹配有限自动机字符串匹配自动机计算状态转移函数代码 五、Knuth-Morris-Pratt算法模式的前缀…

朴素字符串匹配

描述 字符串匹配问题的形式定义&#xff1a; 文本(Text)是一个长度为 n 的字符串:T;模式(Pattern)是一个长度为 m 且 m≤n 的字符串:P; T 和 P 中的元素都属于有限的字母表 Σ 表; 有效位移 (Valid Shift): 如果 0≤ s ≤n-m&#xff0c;并且 T[s1…sm] P[1…m]&#xff0c…

算法之字符串匹配一

目录 前言&#xff1a; BF算法&#xff1a; RK算法 总结&#xff1a; 参考资料 前言&#xff1a; 字符串匹配指的是一个短点的字符串与一个长点的字符串进行匹配&#xff0c;并确认短的字符串是否在长的字符串中存在。匹配算法有很多&#xff0c;本文介绍两种简单、容易…

字符串匹配算法(C语言实现)

目录 文章目录 前言 一、BF算法 二、KMP算法 1.算法介绍 2.算法思路 3.整体代码实现 总结 前言 字符串匹配算法又称模式匹配算法&#xff0c;该算法的目的是为了子串从主串中寻找是否有与其匹配的部分&#xff0c; 其可分为BF暴力检索、RK哈希检索、KMP、BM等等&#xff0c;本…

shell字符串匹配

一、简介 Bash Shell提供了很多字符串和文件处理的命令。如awk、expr、grep、sed等命令&#xff0c;还有文件的排序、合并和分割等一系列的操作命令。grep、sed和awk内容比较多故单独列出&#xff0c;本文只涉及字符串的处理和部分文本处理命令。 二、字符串处理 1、expr命令…

golang字符串匹配算法

简介 字符串匹配算法主要用于在一个较长的文本串中查找一个较短的字符串&#xff08;称为模式串&#xff09;。在 Golang 中&#xff0c;可以使用最常见的字符串匹配算法之一&#xff1a;Knuth-Morris-Pratt&#xff08;KMP&#xff09;算法&#xff0c;它的时间复杂度为 O(nm…

【数据结构】字符串匹配(暴力匹配)

原理解析&#xff1a; 字符串匹配方法&#xff0c;创建两个字符串结构&#xff0c;主 串和子串比较。 主串字符 a 和 子串字符 c 不匹配&#xff0c;主串的指针向下移动&#xff0c;移动到上一次开始比较的下一个位置。 子串指向开始的位置。 主串字符 b 和 子串字符 c 不匹配…

字符串匹配算法比较

字符串匹配&#xff08;string match)是在实际工程中经常会碰到的问题&#xff0c;通常其输入是原字符串(String)和子串&#xff08;又称模式&#xff0c;Pattern)组成&#xff0c;输出为子串在原字符串中的首次出现的位置。通常精确的字符串搜索算法包括暴力搜索(Brute force)…

子串查找(字符串匹配)

子串查询 首先&#xff0c;我们来定义两个概念&#xff0c;主串和模式串。我们在字符串 A 中查找字符串 B&#xff0c;则 A 就是主串&#xff0c;B 就是模式串。我们把主串的长度记为 n&#xff0c;模式串长度记为 m。由于是在主串中查找模式串&#xff0c;因此&#xff0c;主串…

字符串匹配算法综述

字符串匹配算法综述 字符串匹配算法综述&#xff1a;BF、RK、KMP、BM、Sunday 字符串匹配算法&#xff0c;是在实际工程中经常遇到的问题&#xff0c;也是各大公司笔试面试的常考题目。此算法通常输入为原字符串&#xff08;string&#xff09;和子串&#xff08;pattern&…

字符串匹配算法详解

希望看到文章的你们&#xff0c;能够在今年的研究生考试中超常发挥。 愿你们都能考上自己心仪的学校&#xff0c;为你们的备考生涯划上一个完美的句号。做为你们的师兄有几句话想对你们说&#xff0c;希望这些话能对你们有一些帮助。 马上就要考试了&#xff0c;不要再继续啃难…

字符串匹配算法

字符串匹配就是在主串A中查找模式串B&#xff0c;例如在主串abababc中查找模式串abc是否存在&#xff0c;记主串A的长度为n&#xff0c;模式串B的长度为m&#xff0c;n>m。 BF算法 BF(Brute Force)算法&#xff0c;又叫暴力匹配算法或者朴素匹配算法&#xff0c;思路很简单…

字符串(字符串匹配)

一、字符串匹配问题、基础 1、假设文本是一个长度为n的数组T&#xff0c;而模式是长度为m的数组P&#xff0c;我们希望在文本T中寻找模式P 如果P出现在T中的第s个位置&#xff0c;那么我们称其有效偏移为s&#xff0c;在其他不匹配的位置称为无效偏移 2、如果字符串w是字符串…

字符串匹配

字符串匹配 1.朴素的串匹配算法(暴力解法) 1.1 分析 设t是目标串&#xff08;母串&#xff09;&#xff0c;p是模式串&#xff08;待匹配串&#xff09;&#xff0c;i , j 分别指向 模式串 和 目标串&#xff0c;m、n分别是模式串p和目标串t的长度。 从目标串的第0个字符&am…

Photoshop怎么给图片添加简介信息或者版权信息

转自&#xff1a;Photoshop怎么给摄影图片添加作者名字版权等信息? 有时我们点开一张图片的详细信息中可能可以看到各种属性信息&#xff0c;比如作者&#xff0c;时间&#xff0c;关键字&#xff0c;图片信息描述等属性&#xff0c;但是我们自己的拍摄的或者从别的地方获取的…

2022年中国版权保护中心计算机软件著作权登记最全申请步骤流程

一、前言二、实名认证1. 用户注册2. 实名认证 三、办理步骤1. 办理流程2. 填写申请表3. 提交申请文件4. 登记机构受理申请5. 审查6. 获得登记证书 四、登记申请所需文件1. 软件著作权登记申请表2. 软件&#xff08;程序、文档&#xff09;的鉴别材料3. 有关证明文件 五、申请表…

IDEA设置版权信息

File→Settings或者CtrlS快捷键。 Editor下面有个Copyright→Copyright Profiles 点击加号&#xff0c;然后输入名称。 然后修改成自己的信息&#xff1a; 其中第一个年份是本文件新建日期&#xff0c;后面的是最后一次修改年份。 中文版本&#xff1a; 版权所有(c) Jack魏 …

版权和版本信息

版权和版本信息的主要内容有&#xff1a; &#xff08;1&#xff09;版权信息&#xff1b; &#xff08;2&#xff09;文件名称、简要描述、创建日期和作者&#xff1b; &#xff08;3&#xff09;当前版本信息和说明&#xff1b; &#xff08;4&#xff09;历史版本信息和…

版权和商标权有什么关系?版权和商标的区别在哪里?

版权和商标权存在着一定的关系&#xff0c;版权和商标又有着很多区别&#xff0c;具体的关系和区别是怎样的&#xff0c;大家都知道吗&#xff1f;今天企多多就带大家来了解&#xff01; 版权和商标权的关系 版权和商标权的关系主要有以下三点&#xff1a; 1、关联性&#xf…

版权 | 收藏!哪些作品可以登记版权?

创业创新中&#xff0c;无论是公司LOGO还是IP形象或者产品手册&#xff0c;都凝结着无数的心血。当下互联网和科技的发展&#xff0c;让抄袭变得前所未有的容易&#xff0c;尤其在美术作品、文字作品和影视作品领域。如何有效地保护自己的智力成果呢&#xff1f;先从了解这些开…