数据结构与算法之美 | 字符串匹配算法原理讲解(Hash、KMP、BM、Sunday)

简介: 字符串的定位操作通常称作字符串的模式匹配,是各种字符串处理系统中最重要的操作之一,本文介绍Hash、KMP、BM、Sunday四种匹配算法。

0.引言


字符串的定位操作通常称作字符串的模式匹配,是各种字符串处理系统中最重要的操作之一,本文介绍Hash、KMP、BM、Sunday四种匹配算法。


image.png


1. 字符串Hash


字符串Hash就是在字符串上进行哈希,可通俗理解为把字符串转为整数,最后构建理想状态下的一个整数对应一个字符串的单射。


给定一个字符串S,我们规定:


v2-8730a274acf4b842fa1050b56ff6cabb_720w.png


1.1 自然溢出法


自然溢出Hash公式为:


v2-48818f6bc4cd748226080a63fac1df52_720w.png


这里的hash数组利用了unsigned long long的自然溢出对(2^{64} - 1)(264−1)取模。


1.2 单Hash法


单hash公式为:


v2-2a3be38f6a973dcd67b130aff88789de_720w.png


其中p和mod均为素数,且p<mod,为了降低hash冲突,可让p和mod尽量往大取。


当一个哈希值对应两个或多个字符串时出现哈希冲突


取素数可以有效避免冲突,可以参照以下例子:


设函数表达式为f(y, x) = yxmodN


当N = 8,y = 2时:


v2-4c8209fe89c362f35fab77aec99a9141_720w.png


当N = 7,y = 2时:


v2-ae1e8fae3d382f4eeaeff1551b1b2c74_720w.png


可以看出取素数冲突变少了。


1.3 双Hash法


将一个字符串用不同的mod hash两次,再将两个结果用一个二元组表示,构成一个


v2-23bee42c6e91be7b5cc464c576ddafc9_720w.png


1.4 字符串hash的获取


根据定义可以得出:



/

hash[1]=s_1hash[1]=s1
hash[2]=s_1\times p+s_2hash[2]=s1×p+s2
hash[3]=s_1\times p^2+s_2\times p+s_3hash[3]=s1×p2+s2×p+s3
hash[4]=s_1\times p^3+s_2\times p^2+s_3\times p+s_4hash[4]=s1×p3+s2×p2+s3×p+s4


又由


hash[3,4]=s_3\times p+s_4hash[3,4]=s3​×p+s4​
hash[4]=s_1\times p^3+s_2\times p^2+s_3\times p+s_4hash[4]=s1​×p3+s2​×p2+s3​×p+s4​
hash[2]=s_1\times p+s_2hash[2]=s1​×p+s2​


可以得出:


v2-2e1afe6bfc25ef605a7635d2cd352054_720w.png


所以得出通式为:


v2-8bd53042ea3ab24f3ae551c44f611f51_720w.png


又因为每次都要取模,并且做减法时有可能出现负数,所以对其加mod后再取余做出修正,得到求字符串hash的最终公式为:


v2-314bcce3212b3f45f1512a13dad953c3_720w.png


2. KMP


字符串查找算法(Knuth-Morris-Pratt ),简称为KMP算法,常用于在一个文本串S内查找一个模式串P 的出现位置,时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。


KMP算法需要先寻找模式串中最长相等的前缀和后缀lps。


假设模式串P为: “bcdebcd”

可找到的前缀prefix有: “b”、“bc”、“bcd”、“bcde”

可找到的后缀suffix有: “d”、“cd”、“bcd”、“ebcd”

可得出lps为: “bcd”


因此可以根据模式串的lps来建立dp数组储存字符串再次出现的位置:


image.png



设文本串S为"bcbcdbcdbcbcbce",模式串P为"bcbce",模式串的dp数组为[0,0,1,2,0],匹配过程如下:

step1:


image.png


此时S串的i位字符与P串的j+1位字符匹配,i和j分别前进一位。


step2:


image.png


依次进行匹配…


step5:


image.png


此时S串的i位字符为"d",P串的j+1位字符为"e",匹配失败,根据dp数组,字符"c"在P串的2号位出现过,j要从4号位回退到2号位,所以P串向右移动2位,i不变。

step6:


image.png


此时S串的i位字符"d"与P串的j+1位字符"b"失配,因为dp数组中字符"c"在之前没有出现过,则j要回退到0号位,所以P串向右移动2位,i不变。


step7:


image.png


接上一步,S串的i位字符与P串的j+1位字符失配,因为j已经回退到了0号位,不能再回退,所以让i前进一位。

step8:


image.png


再依次进行匹配


step9:


image.png


step10:


image.png


此时S串的i位字符为"d",P串的j+1位字符为"b",匹配失败,根据dp数组要让j回退到0号位,所以模式串向右移动2位,i不变。


step11:


image.png


根据规则再依次进行匹配


step12:


image.png



step19:


image.png


此时S串的i位字符为最后一个字符"e",P串的j位于的4号位,i位字符与j+1位字符匹配,则i和j分别再前进一位。

step20:


image.png


此时i已超出S串的边界,j位于P串的5号位,且等于P串的长度,则整个字符串匹配成功。

如果我们把匹配转移和失配转移用有限状态自动机(FSM)的形式来表示,根据模式串bcbce的dp数组[0,0,1,2,0]可以得出下面一个状态转移图:


v2-14bd78b7ff9eefb91a0159a616e9e2c7_720w.png


注意: 这里进行文本串遍历的i与BF算法(即暴力(Brute Force)算法)进行文本串遍历的i不同,BF算法进行文本串遍历的i是指起始位置,KMP算法进行文本串遍历的i是指当前匹配到的位置。


3. BM


BM(Boyer-Moore)算法则与KMP算法不同,KMP算法的模式串是从前往后遍历,BM算法的模式串是从后往前遍历。BM算法的时间复杂度最好为O(n/m),最坏为O(nm),其中n为文本串的长度,m为模式串的长度。BM算法有两条规则,分别是坏字符串规则(Bad character rule)和好前缀规则(Good suffix rule),根据这两条规则相互竞争来进行字符串匹配。


3.1 坏字符串规则


当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数等于坏字符在模式串对应的位置减去坏字符在模式串中最右出现前的位置(注意该字符要在失配的字符位置之前)。此外,如果"坏字符"不包含在模式串之中,则将模式串直接整体对齐到这个字符的后方,继续比较。坏字符针对的是文本串。


设文本串S为"GCTTCTGCTACCTTTTGCGC",模式串P为"CCTTTTGC",坏字符串匹配规则如下:


当S串中有对应的坏字符"C"时,让P串中最靠右的与坏字符相配的字符"C"与S串中的坏字符对齐,P串相应的向右移动3位(坏字符"C"在P串中的位置4减去坏字符在P串中最右出现的位置1等于3)。


step1:


v2-0da80e452fbebfb9d74c1aa2f54ff95f_720w.png


如果P串中没有出现S串中的那个坏字符,则将模式串直接整体对齐到这个字符的后方,继续比较。


step2:


image.png


移动之后整个字符串匹配成功。


step3:


image.png


3.2 好后缀规则


当字符失配时,后移位数等于好后缀在模式串中的位置减去好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。好后缀针对的是模式串。


设文本串S为"CGTGCCTACTTACTTACTTA",模式串P为"CTTACTTAC",好后缀匹配规则如下:


当P串中存在已经匹配成功的好后缀"TAC",因为要把目标串(上一个"TAC")对齐到好后缀现在的位置,所以P串向右移动的位数为好后缀在P串中的位置6减去好后缀在P串中上一次出现的位置2等于4,然后从P串的最尾元素开始往前匹配。


step1:


image.png


根据规则进行匹配


step2:


image.png


移动之后整个字符串匹配成功。


step3:


image.png


BM算法结合了坏字符串规则(bc)与好后缀规则(gs)来进行遍历,每一步对比两个规则可跳过的字符数,跳过字符数多的则为此步采用的策略,下一步模式串再从右向左依次与文本串相应位置的字符匹配,注意文本串是从左往右遍历,模式串是从右往左遍历。


设文本串S为"GTTATAGCTGATCGCGGCGTAGCGGCGAA",模式串P为"GTAGCGGCG",BM算法匹配过程如下:


初始时刻,P串的最后一个字符与S串对应位置的字符失配,如果采用bc可跳过6个字符,而采用gs则不能跳过字符,所以采用bc将P串向右移动,使模式串中与坏字符相配的字符与坏字符对齐。


step1:


image.png


此刻P串中的最后三个字符"GCG"与S串对应位置的字符匹配,但是到P串的第四个字符则与S串对应的字符失配,如果此时采用bc不能跳过字符,P串只能向右移动1位,但是采用gs可以发现P串中有以之对应的好后缀"GCG",此时可以将前一个"GCG"向后移动到现有"GCG"的位置,跳过的字符数为2位,所以采用gs来移动P串。


step2:


image.png


接上一步之后,P串中的最后六个字符"GCGGCG"与S串对应位置的字符匹配,但是到P串的第七个字符"A"与S串对应位置的字符"C"失配,此时如果采用bc,因为失配的P串位置之前已经没有了与S串中坏字符"C"相配的字符,所以要将整个P串移动到坏字符之后的位置,只跳过了2个字符,但是采用gs则可以跳过7个字符,因为P串中失配的字符位置之前只有一个与好后缀对应的字符"G",所以要将P串开头的"G"对齐到现有最末尾"G"的位置,整体位置移动跳过了7个字符,所以此步采用gs来移动P串。


step3:


image.png


移动之后整个模式串匹配成功。


step4:


image.png


坏字符串规则和好后缀规则可以分开来采用,一般去除坏字符串规则,因为如果字符串很长的话,采用坏字符串规则内存开销非常大。


4. Sunday


Sunday算法的思想和BM算法很相似,只不过Sunday算法中模式串是从左往右匹配,在匹配失败时关注的是主串中参加匹配的最末位字符的下一位字符,规则为:


  • 如果该字符没有在模式串中出现则直接跳过,移动位数等于模式串长度加1
  • 如果该字符在模式串中出现,其移动位数等于模式串长度减去该字符在模式串中最后出现的位置(模式串中字符位置从0开始累加)。


Sunday算法需要建立一个偏移表,偏移表的作用是存储每一个字符的偏移量。设P为模式串,m为模式串长度,w为字符,则偏移量计算公式为:


image.png


例如P=“leetcode”:


m=8
shift[l] = 8 - max(l的位置) = 8 - 0 = 8
shift[e] = 8 - max(e的位置) = 8 - 2 = 6
shift[t] = 8 - max(t的位置) = 8 - 3 = 5
shift[c] = 8 - max(c的位置) = 8 - 4 = 4
shift[o] = 8 - max(o的位置) = 8 - 5 = 3
shift[d] = 8 - max(d的位置) = 8 - 6 = 2
shift[其他] = m + 1 = 8 + 1 = 9


设文本串S为"ATTAAGGCACATAC",模式串P为"ACAT",模式串中字符的偏移量为:


shift[A] = 4 - max(A的位置) = 4 - 2 = 2
shift[C] = 4 - max(C的位置) = 4 - 1 = 3
shift[T] = 4 - max(t的位置) = 4 - 3 = 1


Sunday算法匹配过程如下:


step1:


image.png


此时P串的第2个字符"C"与S串的第2个字符"T"失配,关注S串中参加匹配的最末位字符的下一位字符"A",此时P串中有字符"A",且最后出现在2号位,偏移量为P串的长度4减去字符"A"在P串中最后出现的位置2等于2位,所以将P串向右移动2位。


step2:


image.png


移动之后P串的第1个字符"A"与S串对应位置的字符"T"失配,关注S串中参加匹配的最末位字符的下一位字符"G",但因为P串中没有字符"G",偏移量为P串的长度4加1等于5位,所以P串向右移动5位。


step3:


image.png


移动之后P串的第1个字符"A"与S串对应位置的字符"C"失配,关注S串中参加匹配的最末位字符的下一位字符"A",此时P串中有字符"A",且最后出现在2号位,偏移量为P串的长度4减去2等于2位,所以将P串向右移动2位。


step4:


image.png


移动之后整个字符串匹配成功。


-END-


字符串匹配尽量先写KMP,如果KMP被卡了,就写Hash,如果Hash还不行,就试试BM。虽然Sunday算法很快,但是最常用的还是KMP和Hash。

相关文章
|
23天前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
34 1
|
26天前
|
机器学习/深度学习 算法 数据挖掘
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构
K-means聚类算法是机器学习中常用的一种聚类方法,通过将数据集划分为K个簇来简化数据结构。本文介绍了K-means算法的基本原理,包括初始化、数据点分配与簇中心更新等步骤,以及如何在Python中实现该算法,最后讨论了其优缺点及应用场景。
84 4
|
2月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
92 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
24天前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
24天前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
1月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
97 23
|
1月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
60 20
|
23天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
54 1
|
1月前
|
机器学习/深度学习 算法 C++
数据结构之鲸鱼算法
鲸鱼算法(Whale Optimization Algorithm,WOA)是由伊朗研究员Seyedali Mirjalili于2016年提出的一种基于群体智能的全局优化算法,灵感源自鲸鱼捕食时的群体协作行为。该算法通过模拟鲸鱼的围捕猎物和喷出气泡网的行为,结合全局搜索和局部搜索策略,有效解决了复杂问题的优化需求。其应用广泛,涵盖函数优化、机器学习、图像处理等领域。鲸鱼算法以其简单直观的特点,成为初学者友好型的优化工具,但同时也存在参数敏感、可能陷入局部最优等问题。提供的C++代码示例展示了算法的基本实现和运行过程。
50 0
|
1月前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
49 6