1 前言
本文首发于掘金,有意向转发的同行可私信,想在Android领域有所进步的伙伴,可微信搜索个人的公众号「Android技术集中营」或扫码添加,有不定时福利等大家来拿。
2 Sunday算法
如果各位伙伴们在刷题的时候,碰到关于字符串匹配的问题,在不考虑时间复杂度的情况下,使用暴力匹配这种方式可以搞定,但是一旦在时间复杂度上有了硬性的要求,就需要考虑使用一些魔法了,常见的魔法有KMP算法、Sunday算法,如果网络上搜索KMP算法,大部分文章显得晦涩难懂,一旦出现这种症状,一定是记住了马上忘记, 所以本着打不过"不加入"的原则,我们介绍一下Sunday算法,这个算法效率同样很高,而且比KMP算法能够更加易懂。
题目
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
示例 1:
ini
输入: haystack = "sadbutsad", needle = "sad"
输出: 0
解释: "sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。
示例 2:
arduino
输入: haystack = "leetcode", needle = "leeto"
输出: -1
解释: "leeto" 没有在 "leetcode" 中出现,所以返回 -1 。
2.1 暴力算法解决字符串匹配问题
这道题,没有时间复杂度的要求,属于1🌟难度的问题,显然我们可以使用暴力匹配的方式来完成。
思路
needle
属于模版,需要在haystack
中找到与其匹配的第一个匹配项的下标,那么我们可以定义一个成员变量index
,默认值为-1,如果没有找到匹配项,那么直接返回index
;如果找到了匹配项,那么就将第一个匹配项的下标赋值给index
。
怎么找呢?从haystack
第一个字符开始跟needle
第一个字符开始匹配,如果匹配上了,记下当前位置index
,开始往后比,如果完全匹配,那么返回index
;如果没有匹配上,那么就从index
的下一个位置重新匹配。
java
public int strStr(String haystack, String needle) {
if(haystack.length() < needle.length()){
//一定匹配不上,直接返回-1;
return -1;
}
//这个值记录开始匹配字符串时的下标
int index = -1;
int haystackLength = haystack.length();
int needleLength = needle.length();
//这个参数记录匹配到了字符串的哪个位置
int strPos = 0;
//记录匹配模版的位置
int needleIndex = 0;
while(strPos < haystackLength){
if(index == -1){
if(haystack.charAt(strPos) == needle.charAt(0)){
//与needle第一个字符串匹配上了,意味着这个点,可能会是最终结果
index = strPos;
//即便是匹配上了,但是如果剩余的长度不足以支持needle整体长度判断,也是-1
if(haystackLength - strPos < needleLength){
index = -1;
break;
}
}else{
//如果一直都没匹配上,继续往下走
strPos++;
}
}else{
//正常开始匹配
if(needleIndex == needleLength){
//此时整个needle都匹配完成了,说明找到了
break;
}
if(haystack.charAt(strPos) != needle.charAt(needleIndex)){
//没匹配上,重新计数
strPos = index+1;
needleIndex = 0;
index = -1;
}else{
//如果与模版一致,就继续往下比较
strPos++;
needleIndex++;
}
}
}
return index;
}
这里是通过双指针strPos
和needleIndex
分别记录比较字符串时所处于字符串的位置,而且在遍历字符串时,需要回到字符串的某个位置,因此采用了while循环的方式代替for循环,在匹配的过程中,index
是一个风向标,一旦匹配失败,就需要回到haystack
的index + 1
的位置,继续往下匹配。
这个方法中有一些特殊的情况,需要处理,如果有疑问的伙伴可以评论区留言。
2.2 Sunday算法
前面我们在2.1 小结中,使用了暴力算法实现了字符串的匹配,其中时间复杂度O(n)=m*n,空间复杂度为O(1)。
如果对时间复杂度有了硬性要求,那么这个方法即便是能拿到最终的结果,也是不合理的,这时就需要Sunday算法来实现了。
2.2.1 暴力匹配存在的问题
在2.1小节中,我们通过暴力算法的整个流程是这样的:
此时在strPos位置时,发现t和u没有匹配上,此时需要将index
移到a的位置,继续匹配。
那么既然遍历到t的时候,前面有什么元素其实已经知道了,能不能不退回去再重新匹配呢? 所以Sunday算法的出现,解决了此问题,只通过一次遍历便可以拿到匹配到的字符串。
2.2.2 Sunday算法的精髓
- 与暴力匹配一致,从头开始遍历,直到找到与子串首字母匹配的位置,开始逐个匹配,如果出现不匹配的情况,
例如t和u不匹配,那么就看主串不匹配字符t的下一个字符y,判断y是否在子串中,显然不在,那么直接跳过y,将指针strPos
移动到y的下一个位置a。
- 由于移动之后,第一个字符串就不匹配,相当于匹配失败,依然按照上一步原则,看主串a的下一个字符u,因为u是在子串中的倒数第一的位置 ,因此指针向右移动1位。 此时匹配成功了。
注意这里的规则,是要看字符在子串中倒数的位置,如果子串中有重复出现的字符,那么就需要获取到最后出现这个字符的位置。
java
public int strStr(String haystack, String needle) {
if(haystack.length() < needle.length()){
//一定匹配不上,直接返回-1;
return -1;
}
//这个值记录开始匹配字符串时的下标
int index = -1;
int haystackLength = haystack.length();
int needleLength = needle.length();
//这个参数记录匹配到了字符串的哪个位置
int strPos = 0;
//记录匹配模版的位置
int needleIndex = 0;
//使用Sunday算法
while(strPos < haystackLength){
if(index == -1){
//此时没有匹配成功
if(haystack.charAt(strPos) == needle.charAt(0)){
//匹配到了,会进入到else逻辑
index = strPos;
//即便是匹配到了,但是长度已经不足以后续的匹配
if(haystackLength - strPos < needleLength){
index=-1;
break;
}
}else{
strPos++;
}
}else{
if(needleIndex == needleLength){
break;
}
//正在匹配中
if(haystack.charAt(strPos) != needle.charAt(needleIndex)){
if(index + needleLength >= haystackLength){
//说明此时肯定不会匹配了
index = -1;
break;
}
char nextVal = haystack.charAt(index+needleLength);
if(needle.contains(String.valueOf(nextVal))){
//如果存在,查出来这个字符在子串中的位置
//正向的位置,如果子串中存在重复的元素,拿到最后一个元素
int position = indexOfChar(needle,nextVal);
index = index + needleLength - position;
strPos = index;
index = -1;
needleIndex = 0;
}else{
//如果不存在这个字符,直接跳过
index = index + needleLength + 1;
strPos = index;
index = -1;
needleIndex = 0;
}
}else{
strPos++;
needleIndex++;
}
}
}
return index;
}
public int indexOfChar(String str,char ch){
char[] chars = str.toCharArray();
int result = 0;
for(int i = chars.length-1;i>=0;i--){
if(chars[i] == ch){
result = i;
break;
}
}
return result;
}
其实唯一逻辑的变动,就是在使用暴力算法的时候,如果在匹配的过程中,发现无法匹配,那么就会将主串的strPos进行回溯,回退到index的下一个坐标位置 , 而使用Sunday算法,则是在匹配失败之后,根据规则判断可以跳过几个字符,而不需要回溯,仅需要一次遍历即可。
当然这个算法中存在的不足之处在于,需要通过计算某个字符在字符串中的位置,这个是需要优化的点。