数据结构与算法之美 | 字符串匹配算法原理讲解(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。

相关文章
|
1月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
69 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
30天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
41 3
|
9天前
|
算法 容器
令牌桶算法原理及实现,图文详解
本文介绍令牌桶算法,一种常用的限流策略,通过恒定速率放入令牌,控制高并发场景下的流量,确保系统稳定运行。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
令牌桶算法原理及实现,图文详解
|
19天前
|
负载均衡 算法 应用服务中间件
5大负载均衡算法及原理,图解易懂!
本文详细介绍负载均衡的5大核心算法:轮询、加权轮询、随机、最少连接和源地址散列,帮助你深入理解分布式架构中的关键技术。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
5大负载均衡算法及原理,图解易懂!
|
1月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
25 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
1月前
|
算法
两个字符串匹配出最长公共子序列算法
本文介绍了最长公共子序列(LCS)问题的算法实现,通过动态规划方法求解两个字符串的最长公共子序列,并提供了具体的编程实现细节和示例。
76 1
两个字符串匹配出最长公共子序列算法
|
25天前
|
算法 数据库 索引
HyperLogLog算法的原理是什么
【10月更文挑战第19天】HyperLogLog算法的原理是什么
41 1
|
1月前
|
机器学习/深度学习 人工智能 算法
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
72 0
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
|
1月前
|
搜索推荐 算法
数据结构与算法学习十四:常用排序算法总结和对比
关于常用排序算法的总结和对比,包括稳定性、内排序、外排序、时间复杂度和空间复杂度等术语的解释。
20 0
数据结构与算法学习十四:常用排序算法总结和对比
|
1月前
|
存储 缓存 分布式计算
数据结构与算法学习一:学习前的准备,数据结构的分类,数据结构与算法的关系,实际编程中遇到的问题,几个经典算法问题
这篇文章是关于数据结构与算法的学习指南,涵盖了数据结构的分类、数据结构与算法的关系、实际编程中遇到的问题以及几个经典的算法面试题。
29 0
数据结构与算法学习一:学习前的准备,数据结构的分类,数据结构与算法的关系,实际编程中遇到的问题,几个经典算法问题