字符串匹配(查找)算法是一类重要的字符串算法(String Algorithm)。有两个字符串, 长度为m的haystack(查找串)和长度为n的needle(模式串), 它们构造自同一个有限的字母表(Alphabet)。如果在haystack中存在一个与needle相等的子串,返回子串的起始下标,否则返回-1。C/C++、PHP中的strstr函数实现的就是这一功能。LeetCode上也有类似的题目,比如#28#187.

这个问题已经被研究了n多年,出现了很多高效的算法,比较著名的有,Knuth-Morris-Pratt 算法 (KMP)、Boyer-Moore搜索算法、Rabin-Karp算法、Sunday算法等。

Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很相似, 其效率在匹配随机的字符串时不仅比其它匹配算法更快,而且 Sunday 算法 的实现比 KMP、BM 的实现容易很多!

只不过Sunday算法是从前往后匹配,在匹配失败时关注的是主串中参加匹配的最末位字符的下一位字符。

如果该字符没有在模式串中出现则直接跳过,即移动位数 = 模式串长度 + 1;
否则,其移动位数 = 模式串长度 - 该字符最右出现的位置(以0开始) = 模式串中该字符最右出现的位置到尾部的距离 + 1。

先说暴力法:两个串左端对其,然后从needle的最左边字符往右逐一匹配,如果出现失配,则将needle往右移动一位,继续从needle左端开始匹配...如此,直到找到一串完整的匹配,或者haystack结束。时间复杂度是O(mn),看起来不算太糟。入下图所示:
图中红色标记的字母表示第一个发生失配的位置,绿色标记的是完整匹配的位置。

重复这个匹配、右移的过程,每次只将needle右移一个位置

直到找到这么个完整匹配的子串。

限制这个算法效率的因素在于,有很多重复的不必要的匹配尝试。因此想办法减少不必要的匹配,就能提高效率咯。很多高效的字符串匹配算法,它们的核心思想都是一样样的,想办法利用部分匹配的信息,减少不必要的尝试。

Sunday算法利用的是发生失配时查找串中的下一个位置的字母。还是用图来说明:

上图的查找中,在haystack[1]和needle[1]的位置发生失配,接下来要做的事情,就是把needle右移。在右移之前我们先把注意力haystack[3]=d这个位置上。如果needle右移一位,needle[2]=c跟haystack[3]对应,如果右移两位,needle[1]=b跟haystack[3]对应,如果移三位,needle[0]=a跟haystack[3]对应。然后无论以上情况中的哪一种,在haystack[3]这个位置上都会失配(当然在这个位置前面也可能失配),因为haystack[3]=d这个字母根本就不存在于needle中。因此更明智的做法应该是直接移四位,变成这样:

然后我们发现在needle[0]=a,haystack[4]=b位置又失配了,于是沿用上一步的思路,看看haystack[7]=b。这次我们发现字母b是在needle中存在的,那它就有可能形成一个完整的匹配,因为我们完全直接跳过,而应该跳到haystack[7]与needle[1]对应的位置,如下图:

这一次,我们差点就找到了一个完整匹配,可惜needle[0]的位置失配了。不要气馁,再往后,看haystack[9]=z的位置,它不存在于needle中,于是跳到z的下一个位置,然后...:

于是我们顺利地找到了一个匹配!
然后试着从上面的过程中总结出一个算法来。

输入: haystack, needle
Init: i=0, j=0
while i<=len(haystack)-len(needle):
j=0
while j<len(needle) and haystack[i+j] equals needle[j]:
j=j+1
if j equals len(needle):
return i
else
increase i...

这里有一个问题,发生失配时,i应该增加多少。如果haystack[i+j]位置的字母不存在于needle中,我们知道可以跳到i+j+1的位置。而如果chr=haystack[i+j]存在于needle,我们说可以跳到使chr对应needle中的同一个字母的位置。但问题是,needle中可能有不止一个的字母等于chr。这种情况下,应该跳到哪一个位置呢?为了不遗漏可能的匹配,应该是跳到使得needle中最右一个chr与haystack[i+j]对应,这样跳过的距离最小,且是安全的。
于是我们知道,在开始查找之前,应该做一项准备工作,收集Alphabet中的字母在needle中最右一次出现的位置。我们建立一个O(k)这么大的数组,k是Alphabet的大小,这个数组记录了每一个字母在needle中最右出现的位置。遍历needle,更新对应字母的位置,如果一个字母出现了两次,前一个位置就会被后一个覆盖,另外我们用-1表示根本不在needle中出现。
用occ表示这个位置数组,求occ的过程如下:

输入: needle
Init: occ is a integer array whose size equals len(needle)
fill occ with -1
i=0
while i<len(needle):
occ[needle[i]]=i
return occ

还有一点需要注意的是,Sunday算法并不限制对needle串的匹配顺序,可以从左往右扫描needle,可以从右往左,甚至任何自定义的顺序。
接下来尝试具体实现一下这个算法,以下是Java程序,这里假设Alphabet就是ASCII字符集。

算法的时间复杂度主要依赖两个因素,一是i每次能跳过的位置有多少;二是在内部循环尝试匹配时,多快能确定是失配了还是完整匹配了。在最好的情况下,每次失配,occ[haystack[i+j]]都是-1,于是每次i都跳过n+1个位置;并且当在内部循环尝试匹配,总能在第一个字符位置就确定失配了,这样得到时间O(m/n)。比如下图这种情况:

最坏情况下,每次i都只能移动一位,且总是几乎要到needle的末尾才发现失配了。时间复杂度是O(m*n)并不比Brut-force的解法好。比如像这样:

使用Alphabet解法:

class Solution {

    public int strStr(String haystack, String needle) {
int m=haystack.length(), n=needle.length();
int[] occ=getOCC(needle);
int jump=;
for(int i=;i<=m-n; i+=jump){
int j=;
while(j<n&&haystack.charAt(i+j)==needle.charAt(j))
j++;
if(j==n)
return i;
jump=i+n<m ? n-occ[haystack.charAt(i+n)] : ;
}
return -;
} public int[] getOCC(String p){
int[] occ=new int[];
for(int i=;i<occ.length;i++)
occ[i]=-;
for(int i=;i<p.length();i++)
occ[p.charAt(i)]=i;
return occ;
}
}
不用Alphabet的解法 by Golang:
package main

import "fmt"

func strStr(haystack string, needle string) int {
if haystack == needle {
return
}
nl:=len(needle)
if nl=={
return
}
hl:=len(haystack)
if hl=={
return -
}
nm:=map[byte]int{}
for i:=;i<nl;i++{
nm[needle[i]]=i
}
fmt.Printf("%+#v %v %v ",nm, haystack, needle)
for i :=; i <hl;{
j:=
tmp:=i
for ;j<nl && tmp<hl;j++{
if haystack[tmp] != needle[j] {
break
}
tmp++
}
fmt.Printf("i %v, j %v ", i,j)
if j == nl {
return i
}else if i+nl<hl {
hit,exists := nm[haystack[i+nl]]
fmt.Printf("alpha %v hit %v exst %v ",string(haystack[i+nl]),hit, exists)
if exists {
i+=nl-hit
}else{
i+=nl+
}
}else {
return -
}
}
return -
}
func main(){
fmt.Println(strStr("a","a"))
fmt.Println(strStr("abcdeabc","abcab"))
fmt.Println(strStr("abcdeabc","abcabe"))
fmt.Println(strStr("abcebc","abc"))
fmt.Println(strStr("nnabcd e aebc","abc"))
fmt.Println(strStr("mississippi","issi"))
fmt.Println(strStr("mississippi","issip"))
}

 

前面提到Sunday算法对needle的扫描顺序是没有限制的。为了提高在最坏情况下的算法效率,可以对needle中的字符按照其出现的概率从小到大的顺序扫描,这样能尽早地确定失配与否。
Sunday算法实际上是对Boyer-Moore算法的优化,并且它更简单易实现。其论文中提出了三种不同的算法策略,结果都优于Boyer-Moore算法。

Reference:
1] [D.M. Sunday: A Very Fast Substring Search Algorithm. Communications of the ACM, 33, 8, 132-142 (1990)
2] [Fachhochschule Flensburg

通用高效字符串匹配--Sunday算法的更多相关文章

  1. 字符串匹配 - sunday算法

    常见的字符串匹配算法有BF.KMP(教科书中非常经典的).BM.Sunday算法 这里主要想介绍下性能比较好并且实现比较简单的Sunday算法 . 基本原理: 从前往后匹配,如果遇到不匹配情况判断母串 ...

  2. 字符串匹配KMP算法详解

    1. 引言 以前看过很多次KMP算法,一直觉得很有用,但都没有搞明白,一方面是网上很少有比较详细的通俗易懂的讲解,另一方面也怪自己没有沉下心来研究.最近在leetcode上又遇见字符串匹配的题目,以此 ...

  3. 字符串匹配Boyer-Moore算法:文本编辑器中的查找功能是如何实现的?---这应该讲的最容易懂的文章了!

    关于字符串匹配算法有很多,之前我有讲过一篇 KMP 匹配算法:图解字符串匹配 KMP 算法,不懂 kmp 的建议看下,写的还不错,这个算法虽然很牛逼,但在实际中用的并不是特别多.至于选择哪一种字符串匹 ...

  4. 字符串匹配常见算法(BF,RK,KMP,BM,Sunday)

    今日了解了一下字符串匹配的各种方法. 并对sundaysearch算法实现并且单元. 字符串匹配算法,是在实际工程中经常遇到的问题,也是各大公司笔试面试的常考题目.此算法通常输入为原字符串(strin ...

  5. 字符串匹配--Karp-Rabin算法

    主要特征 1.使用hash函数 2.预处理阶段时间复杂度O(m),常量空间 3.查找阶段时间复杂度O(mn) 4.期望运行时间:O(n+m) 本文地址:http://www.cnblogs.com/a ...

  6. 字符串匹配KMP算法

    1. 字符串匹配的KMP算法 2. KMP算法详解 3. 从头到尾彻底理解KMP

  7. 字符串匹配--kmp算法原理整理

    kmp算法原理:求出P0···Pi的最大相同前后缀长度k: 字符串匹配是计算机的基本任务之一.举例,字符串"BBC ABCDAB ABCDABCDABDE",里面是否包含另一个字符 ...

  8. 字符串匹配KMP算法的C语言实现

    字符串匹配是计算机的基本任务之一. 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD" ...

  9. 字符串匹配KMP算法的讲解C++

    转自http://blog.csdn.net/starstar1992/article/details/54913261 也可以参考http://blog.csdn.net/liu940204/art ...

随机推荐

  1. [Linux]F5负载均衡器

    F5负载均衡器是硬件的负载均衡设备 F5配置最简单负载均衡,需要配置的参数有Node(节点).Pool(资源池).和Virtual Server(虚拟服务器),它们的关系是,先配置Node,然后配置V ...

  2. JAVAWEB复习-JS

    1.概述 JavaScript是基于对象和事件的脚本语言,主要应用在客户端 特点:信息动态交互,不可直接访问本地磁盘,只要是可以解析js的浏览器都可以跨平台执行 2.JS和JAVA比较 a:JS是面向 ...

  3. 201871010109-胡欢欢《面向对象程序设计(java)》第十三周学习总结

    项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/p ...

  4. 201871010121-王方-《面向对象程序开发设计java》第十四周实验总结

    4 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh 这个作业的要求在哪里 https://www.cnblogs.com/nwnu-daizh/ ...

  5. jenkins slave上执行脚本报错

    jenkins slave上执行脚本报错 解决方法:在系统配置中设置shell execuate C:\Windows\system32\cmd.exe 保存即可

  6. adb连接夜神模拟器与adb常用操作命令

    adb connect 127.0.0.1:62001 adb kill-server 在关闭adb服务后,要使用如下的命令启动adb服务. adb start-servermore than one ...

  7. Java8——Stream

    /* * 一.Stream API 的操作步骤: * * 1. 创建 Stream * * 2. 中间操作 * * 3. 终止操作(终端操作) */ public class TestStreamaA ...

  8. VIJOS-P1474 雷曼兔(csapc)

    JDOJ 1532: VIJOS-P1474 雷曼兔(csapc) https://neooj.com/oldoj/problem.php?id=1532 Description 这次,OI山成为了雷 ...

  9. JAVA List中剔除空元素(null)的方法

    方法一.list.removeAll(Collections.singleton(null)); 方法二.List nullList = new ArrayList();                ...

  10. 理解Javascript执行过程

    Javascript是一种解释型的动态语言. 在程序中,有编译型语言和解释型语言.那么什么是编译型语言,什么是解释型语言呢? 编译型语言: 它首先将源代码编译成机器语言,再由机器运行机器码(二进制). ...